agent-migrate-adapters 0.6.1

Codex, Pi, and Claude Code configuration adapters for agent-migrate
Documentation
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(&current)
            .map(String::into_bytes)
            .map_err(|error| error.to_string()),
        AgentKind::Pi | AgentKind::Claude => {
            serde_json::to_vec_pretty(&current).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())
        }
    }
}