use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value, json};
use crate::installer::instructions_template::{
RUSTCODEGRAPH_INSTRUCTIONS_BLOCK, RUSTCODEGRAPH_SECTION_END, RUSTCODEGRAPH_SECTION_START,
};
use super::types::{FileWrite, WriteAction, file_write};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct McpServerConfig {
#[serde(rename = "type")]
pub type_name: String,
pub command: String,
pub args: Vec<String>,
}
pub fn get_mcp_server_config() -> McpServerConfig {
McpServerConfig {
type_name: "stdio".to_owned(),
command: "rustcodegraph".to_owned(),
args: vec!["serve".to_owned(), "--mcp".to_owned()],
}
}
pub fn get_code_graph_permissions() -> Vec<String> {
[
"mcp__rustcodegraph__rustcodegraph_explore",
"mcp__rustcodegraph__rustcodegraph_search",
"mcp__rustcodegraph__rustcodegraph_node",
"mcp__rustcodegraph__rustcodegraph_callers",
"mcp__rustcodegraph__rustcodegraph_callees",
"mcp__rustcodegraph__rustcodegraph_impact",
"mcp__rustcodegraph__rustcodegraph_files",
"mcp__rustcodegraph__rustcodegraph_status",
]
.into_iter()
.map(str::to_owned)
.collect()
}
pub fn home_dir() -> PathBuf {
env::var_os("HOME")
.or_else(|| env::var_os("USERPROFILE"))
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."))
}
pub fn current_dir() -> PathBuf {
env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
}
pub fn path_to_string(path: impl AsRef<Path>) -> String {
path.as_ref().to_string_lossy().into_owned()
}
pub fn file_exists(path: impl AsRef<Path>) -> bool {
path.as_ref().exists()
}
pub fn read_text(path: impl AsRef<Path>) -> String {
fs::read_to_string(path).unwrap_or_default()
}
pub fn read_json_file(path: impl AsRef<Path>) -> Value {
let path = path.as_ref();
if !path.exists() {
return Value::Object(Map::new());
}
fs::read_to_string(path)
.ok()
.and_then(|raw| serde_json::from_str::<Value>(&raw).ok())
.filter(Value::is_object)
.unwrap_or_else(|| Value::Object(Map::new()))
}
pub fn write_json_file(path: impl AsRef<Path>, data: &Value) -> std::io::Result<()> {
let content = serde_json::to_string_pretty(data).unwrap_or_else(|_| "{}".to_owned()) + "\n";
atomic_write_file_sync(path, &content)
}
pub fn atomic_write_file_sync(path: impl AsRef<Path>, content: &str) -> std::io::Result<()> {
let path = path.as_ref();
if let Some(dir) = path.parent() {
fs::create_dir_all(dir)?;
}
let tmp = path.with_extension(format!(
"{}tmp.{}",
path.extension()
.and_then(|ext| ext.to_str())
.map(|ext| format!("{ext}."))
.unwrap_or_default(),
std::process::id()
));
fs::write(&tmp, content)?;
fs::rename(tmp, path)
}
pub fn json_deep_equal(a: &Value, b: &Value) -> bool {
a == b
}
pub fn get_nested<'a>(value: &'a Value, path: &[&str]) -> Option<&'a Value> {
let mut cur = value;
for part in path {
cur = cur.get(*part)?;
}
Some(cur)
}
pub fn has_nested(value: &Value, path: &[&str]) -> bool {
get_nested(value, path).is_some()
}
pub fn mcp_server_value() -> Value {
serde_json::to_value(get_mcp_server_config()).unwrap_or_else(|_| {
json!({
"type": "stdio",
"command": "rustcodegraph",
"args": ["serve", "--mcp"]
})
})
}
pub fn mcp_json_snippet(server: Value) -> String {
serde_json::to_string_pretty(&json!({ "mcpServers": { "rustcodegraph": server } }))
.unwrap_or_else(|_| "{\n \"mcpServers\": {}\n}".to_owned())
}
pub fn planned_json_mcp_write(path: impl AsRef<Path>, server: Value) -> FileWrite {
let path_ref = path.as_ref();
let mut existing = read_json_file(path_ref);
let before = get_nested(&existing, &["mcpServers", "rustcodegraph"]);
let action = if before.is_some_and(|value| json_deep_equal(value, &server)) {
WriteAction::Unchanged
} else if before.is_some() || path_ref.exists() {
WriteAction::Updated
} else {
WriteAction::Created
};
if action != WriteAction::Unchanged {
if !existing.is_object() {
existing = Value::Object(Map::new());
}
let root = existing.as_object_mut().expect("root JSON is an object");
let servers = root
.entry("mcpServers")
.or_insert_with(|| Value::Object(Map::new()));
if !servers.is_object() {
*servers = Value::Object(Map::new());
}
servers
.as_object_mut()
.expect("mcpServers JSON is an object")
.insert("rustcodegraph".to_owned(), server);
let _ = write_json_file(path_ref, &existing);
}
file_write(path_to_string(path_ref), action)
}
pub fn planned_json_mcp_remove(path: impl AsRef<Path>) -> FileWrite {
let path_ref = path.as_ref();
if !path_ref.exists() {
return file_write(path_to_string(path_ref), WriteAction::NotFound);
}
let mut existing = read_json_file(path_ref);
let mut removed = false;
if let Some(servers) = existing
.get_mut("mcpServers")
.and_then(|value| value.as_object_mut())
{
removed = servers.remove("rustcodegraph").is_some();
if servers.is_empty() {
existing
.as_object_mut()
.and_then(|root| root.remove("mcpServers"));
}
}
let action = if removed {
let _ = write_json_file(path_ref, &existing);
WriteAction::Removed
} else {
WriteAction::NotFound
};
file_write(path_to_string(path_ref), action)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MarkedSectionAction {
Created,
Updated,
Appended,
Unchanged,
}
pub fn replace_or_append_marked_section(
path: impl AsRef<Path>,
body: &str,
start_marker: &str,
end_marker: &str,
) -> MarkedSectionAction {
let path = path.as_ref();
if !path.exists() {
let _ = atomic_write_file_sync(path, &(body.to_owned() + "\n"));
return MarkedSectionAction::Created;
}
let Ok(content) = fs::read_to_string(path) else {
return MarkedSectionAction::Updated;
};
let Some(start) = content.find(start_marker) else {
return MarkedSectionAction::Appended;
};
let Some(end_rel) = content[start..].find(end_marker) else {
return MarkedSectionAction::Appended;
};
let end = start + end_rel + end_marker.len();
if &content[start..end] == body {
MarkedSectionAction::Unchanged
} else {
let before = &content[..start];
let after = &content[end..];
let _ = atomic_write_file_sync(path, &format!("{before}{body}{after}"));
MarkedSectionAction::Updated
}
}
pub fn upsert_instructions_entry(path: impl AsRef<Path>) -> FileWrite {
let path_ref = path.as_ref();
let action =
match replace_or_append_any_marked_section(path_ref, RUSTCODEGRAPH_INSTRUCTIONS_BLOCK) {
MarkedSectionAction::Created => WriteAction::Created,
MarkedSectionAction::Updated | MarkedSectionAction::Appended => WriteAction::Updated,
MarkedSectionAction::Unchanged => WriteAction::Unchanged,
};
file_write(path_to_string(path_ref), action)
}
fn replace_or_append_any_marked_section(path: &Path, body: &str) -> MarkedSectionAction {
if !path.exists() {
let _ = atomic_write_file_sync(path, &(body.to_owned() + "\n"));
return MarkedSectionAction::Created;
}
let Ok(content) = fs::read_to_string(path) else {
return MarkedSectionAction::Updated;
};
if let Some(start) = content.find(RUSTCODEGRAPH_SECTION_START)
&& let Some(end_rel) = content[start..].find(RUSTCODEGRAPH_SECTION_END)
{
let end = start + end_rel + RUSTCODEGRAPH_SECTION_END.len();
if &content[start..end] == body {
return MarkedSectionAction::Unchanged;
}
let before = &content[..start];
let after = &content[end..];
let _ = atomic_write_file_sync(path, &format!("{before}{body}{after}"));
return MarkedSectionAction::Updated;
}
let trimmed = content.trim_end();
let sep = if trimmed.is_empty() { "" } else { "\n\n" };
let _ = atomic_write_file_sync(path, &format!("{trimmed}{sep}{body}\n"));
MarkedSectionAction::Appended
}
pub fn remove_marked_section(
path: impl AsRef<Path>,
start_marker: &str,
end_marker: &str,
) -> WriteAction {
let path = path.as_ref();
if !path.exists() {
return WriteAction::Kept;
}
let Ok(content) = fs::read_to_string(path) else {
return WriteAction::Kept;
};
let Some(start) = content.find(start_marker) else {
return WriteAction::NotFound;
};
let Some(end_rel) = content[start..].find(end_marker) else {
return WriteAction::NotFound;
};
if end_rel == 0 {
return WriteAction::NotFound;
}
let end = start + end_rel + end_marker.len();
let before = content[..start].trim_end();
let after = content[end..].trim_start();
let joined = if before.is_empty() {
after.to_owned()
} else if after.is_empty() {
before.to_owned()
} else {
format!("{before}\n\n{after}")
};
if joined.trim().is_empty() {
let _ = fs::remove_file(path);
} else {
let _ = atomic_write_file_sync(path, &(joined.trim().to_owned() + "\n"));
}
WriteAction::Removed
}
pub fn remove_rustcodegraph_instructions(path: impl AsRef<Path>) -> FileWrite {
let path_ref = path.as_ref();
let action = remove_marked_section(
path_ref,
RUSTCODEGRAPH_SECTION_START,
RUSTCODEGRAPH_SECTION_END,
);
file_write(path_to_string(path_ref), action)
}