use std::path::Path;
use agent_migrate_core::{AgentKind, FileKind, PathMappingError, PathMappingSet};
pub(crate) fn map_paths(
kind: FileKind,
content: &[u8],
mappings: &PathMappingSet,
) -> Result<Vec<u8>, PathMappingError> {
if !matches!(kind, FileKind::Settings | FileKind::Mcp | FileKind::Hook) {
return Ok(content.to_vec());
}
let mut value = serde_json::from_slice(content).map_err(|_| PathMappingError::Invalid)?;
map_value(&mut value, None, mappings)?;
serde_json::to_vec_pretty(&value).map_err(|_| PathMappingError::Invalid)
}
fn map_value(
value: &mut serde_json::Value,
key: Option<&str>,
mappings: &PathMappingSet,
) -> Result<(), PathMappingError> {
match value {
serde_json::Value::Object(object) => {
for (child_key, child) in object {
map_value(child, Some(child_key), mappings)?;
}
}
serde_json::Value::Array(values) => {
for child in values {
map_value(child, key, mappings)?;
}
}
serde_json::Value::String(text)
if key.is_some_and(path_key) && Path::new(text).is_absolute() =>
{
*text = mappings
.map(Path::new(text))?
.to_string_lossy()
.into_owned();
}
_ => {}
}
Ok(())
}
fn path_key(key: &str) -> bool {
let key = key.to_ascii_lowercase();
key == "path"
|| key == "cwd"
|| key == "root"
|| key == "workspace"
|| key.ends_with("_path")
|| key.ends_with("_dir")
|| key.ends_with("_directory")
}
pub(crate) fn render_content(
target: AgentKind,
kind: FileKind,
content: &[u8],
) -> Result<Vec<u8>, String> {
match kind {
FileKind::Mcp => render_mcp(target, content),
FileKind::Settings => render_settings(target, content),
_ => Ok(content.to_vec()),
}
}
pub(crate) fn merge_rendered(
target: AgentKind,
current: &[u8],
incoming: &[u8],
) -> Result<Vec<u8>, String> {
let mut current = parse_rendered(target, current)?;
let incoming = parse_rendered(target, incoming)?;
merge_values(&mut current, incoming);
match target {
AgentKind::Codex => toml_edit::ser::to_string_pretty(¤t)
.map(String::into_bytes)
.map_err(|error| error.to_string()),
AgentKind::Pi | AgentKind::Claude => {
serde_json::to_vec_pretty(¤t).map_err(|error| error.to_string())
}
}
}
fn parse_rendered(target: AgentKind, content: &[u8]) -> Result<serde_json::Value, String> {
let text = std::str::from_utf8(content).map_err(|error| error.to_string())?;
match target {
AgentKind::Codex => toml_edit::de::from_str(text).map_err(|error| error.to_string()),
AgentKind::Pi | AgentKind::Claude => {
serde_json::from_str(text).map_err(|error| error.to_string())
}
}
}
fn merge_values(current: &mut serde_json::Value, incoming: serde_json::Value) {
match (current, incoming) {
(serde_json::Value::Object(current), serde_json::Value::Object(incoming)) => {
for (key, value) in incoming {
if let Some(existing) = current.get_mut(&key) {
merge_values(existing, value);
} else {
current.insert(key, value);
}
}
}
(current, incoming) => *current = incoming,
}
}
fn render_settings(target: AgentKind, content: &[u8]) -> Result<Vec<u8>, String> {
let value: serde_json::Value =
serde_json::from_slice(content).map_err(|error| error.to_string())?;
match target {
AgentKind::Codex => toml_edit::ser::to_string_pretty(&value)
.map(String::into_bytes)
.map_err(|error| error.to_string()),
AgentKind::Pi | AgentKind::Claude => {
serde_json::to_vec_pretty(&value).map_err(|error| error.to_string())
}
}
}
fn render_mcp(target: AgentKind, content: &[u8]) -> Result<Vec<u8>, String> {
let value: serde_json::Value =
serde_json::from_slice(content).map_err(|error| error.to_string())?;
let servers = value
.get("mcpServers")
.or_else(|| value.get("mcp_servers"))
.cloned()
.unwrap_or(value);
let key = match target {
AgentKind::Codex => "mcp_servers",
AgentKind::Pi => "servers",
AgentKind::Claude => "mcpServers",
};
let root = if target == AgentKind::Pi {
serde_json::json!({ "version": 1, key: servers })
} else {
serde_json::json!({ key: servers })
};
match target {
AgentKind::Codex => toml_edit::ser::to_string_pretty(&root)
.map(String::into_bytes)
.map_err(|error| error.to_string()),
AgentKind::Pi | AgentKind::Claude => {
serde_json::to_vec_pretty(&root).map_err(|error| error.to_string())
}
}
}