robotrt-cli 0.1.0-beta.1

RobotRT modular robotics runtime and middleware components.
pub(in crate::commands::sdk) fn parse_schema_version(content: &str) -> Option<u64> {
    let mut in_project = false;
    for line in content.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with('[') && trimmed.ends_with(']') {
            in_project = trimmed == "[project]";
            continue;
        }
        if in_project && trimmed.starts_with("schema_version") {
            let raw = trimmed.split('=').nth(1)?.trim();
            return raw.parse::<u64>().ok();
        }
    }
    None
}

pub(in crate::commands::sdk) fn parse_project_template(content: &str) -> Option<String> {
    let mut in_project = false;
    for line in content.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with('[') && trimmed.ends_with(']') {
            in_project = trimmed == "[project]";
            continue;
        }
        if in_project && trimmed.starts_with("template") {
            return parse_quoted_assignment(trimmed);
        }
    }
    None
}

pub(in crate::commands::sdk) fn parse_quoted_assignment(line: &str) -> Option<String> {
    let value = line.split('=').nth(1)?.trim();
    if value.len() < 2 {
        return None;
    }
    if value.starts_with('"') && value.ends_with('"') {
        return Some(value[1..value.len() - 1].to_string());
    }
    None
}

pub(in crate::commands::sdk) fn migrate_robotrt_config_chain(
    content: &str,
    from_schema_version: u64,
    target_schema_version: u64,
    fallback_name: &str,
) -> (String, Vec<String>) {
    if target_schema_version <= from_schema_version {
        return (content.to_string(), Vec::new());
    }

    let mut out = ensure_project_section(content, fallback_name);
    let mut steps = Vec::new();
    let mut current = from_schema_version.max(1);

    while current < target_schema_version {
        let next = current + 1;
        out = match next {
            2 => {
                let upgraded = set_or_insert_key(&out, "project", "schema_version", "2");
                steps.push(String::from(
                    "v1 -> v2: add/normalize [project].schema_version",
                ));
                upgraded
            }
            3 => {
                let with_runtime = set_or_insert_key(&out, "runtime", "profile", "\"balanced\"");
                let upgraded = set_or_insert_key(&with_runtime, "project", "schema_version", "3");
                steps.push(String::from("v2 -> v3: add [runtime].profile=\"balanced\""));
                upgraded
            }
            4 => {
                let with_ops = set_or_insert_key(
                    &out,
                    "ops",
                    "status_api_version",
                    "\"robotrt.status.service.v1\"",
                );
                let upgraded = set_or_insert_key(&with_ops, "project", "schema_version", "4");
                steps.push(String::from(
                    "v3 -> v4: add [ops].status_api_version and project schema_version",
                ));
                upgraded
            }
            version => {
                let upgraded =
                    set_or_insert_key(&out, "project", "schema_version", &version.to_string());
                steps.push(format!(
                    "v{} -> v{}: set [project].schema_version",
                    current, version
                ));
                upgraded
            }
        };
        current = next;
    }

    (ensure_trailing_newline(&out), steps)
}

pub(in crate::commands::sdk) fn ensure_project_section(
    content: &str,
    fallback_name: &str,
) -> String {
    if content.trim().is_empty() {
        return format!(
            "[project]\nname = \"{}\"\ntemplate = \"local\"\nschema_version = 1\n",
            fallback_name
        );
    }

    if content.lines().any(|line| line.trim() == "[project]") {
        return ensure_trailing_newline(content);
    }

    let template = parse_project_template(content).unwrap_or_else(|| "local".to_string());
    format!(
        "{}\n[project]\nname = \"{}\"\ntemplate = \"{}\"\nschema_version = 1\n",
        content.trim_end(),
        fallback_name,
        template
    )
}

pub(in crate::commands::sdk) fn set_or_insert_key(
    content: &str,
    section: &str,
    key: &str,
    value: &str,
) -> String {
    let mut out = Vec::new();
    let mut in_target = false;
    let mut section_seen = false;
    let mut key_seen = false;

    for line in content.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with('[') && trimmed.ends_with(']') {
            if in_target && !key_seen {
                out.push(format!("{key} = {value}"));
                key_seen = true;
            }

            let section_name = trimmed.trim_matches(&['[', ']'][..]);
            in_target = section_name == section;
            if in_target {
                section_seen = true;
            }

            out.push(line.to_string());
            continue;
        }

        if in_target && trimmed.starts_with(key) {
            out.push(format!("{key} = {value}"));
            key_seen = true;
            continue;
        }

        out.push(line.to_string());
    }

    if in_target && !key_seen {
        out.push(format!("{key} = {value}"));
        key_seen = true;
    }

    if !section_seen {
        if !out.is_empty() && !out.last().is_some_and(|line| line.is_empty()) {
            out.push(String::new());
        }
        out.push(format!("[{section}]"));
        out.push(format!("{key} = {value}"));
    } else if !key_seen {
        out.push(format!("{key} = {value}"));
    }

    ensure_trailing_newline(&out.join("\n"))
}

pub(in crate::commands::sdk) fn ensure_trailing_newline(content: &str) -> String {
    if content.ends_with('\n') {
        content.to_string()
    } else {
        format!("{content}\n")
    }
}