robotrt-cli 0.1.0-beta.1

RobotRT modular robotics runtime and middleware components.
use super::*;

pub(in crate::commands::sdk) fn execute_sdk_rollback(
    project_dir: PathBuf,
    backup_override: Option<PathBuf>,
) -> Result<(PathBuf, PathBuf, PathBuf), String> {
    let config_path = project_dir.join("robotrt.toml");
    let backup_path = backup_override.unwrap_or_else(|| project_dir.join("robotrt.toml.bak"));

    if !backup_path.exists() {
        return Err(format!("backup file not found: {}", backup_path.display()));
    }
    if !config_path.exists() {
        return Err(format!("config file not found: {}", config_path.display()));
    }

    let rollback_backup_path = project_dir.join("robotrt.toml.rollback.bak");
    let current = fs::read_to_string(&config_path)
        .map_err(|err| format!("read {} failed: {err}", config_path.display()))?;
    fs::write(&rollback_backup_path, current).map_err(|err| {
        format!(
            "write rollback backup {} failed: {err}",
            rollback_backup_path.display()
        )
    })?;

    let backup = fs::read_to_string(&backup_path)
        .map_err(|err| format!("read {} failed: {err}", backup_path.display()))?;
    fs::write(&config_path, backup)
        .map_err(|err| format!("restore config {} failed: {err}", config_path.display()))?;

    Ok((config_path, backup_path, rollback_backup_path))
}

pub(in crate::commands::sdk) fn write_scaffold(
    project_dir: &Path,
    project_name: &str,
    template: &str,
    workspace_root: &Path,
) -> Result<(), String> {
    fs::create_dir_all(project_dir.join("src")).map_err(|err| {
        format!(
            "create project directory {} failed: {err}",
            project_dir.display()
        )
    })?;

    write_file(
        &project_dir.join("Cargo.toml"),
        &render_cargo_toml(project_name, workspace_root),
    )?;
    write_file(
        &project_dir.join("README.md"),
        &render_readme(project_name, template),
    )?;
    write_file(
        &project_dir.join("robotrt.toml"),
        &render_robotrt_config(project_name, template),
    )?;
    write_file(&project_dir.join(".gitignore"), "target/\n")?;
    write_file(
        &project_dir.join("src/main.rs"),
        &render_main_rs(project_name, template),
    )?;

    Ok(())
}

pub(in crate::commands::sdk) fn write_file(path: &Path, content: &str) -> Result<(), String> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)
            .map_err(|err| format!("create directory {} failed: {err}", parent.display()))?;
    }

    fs::write(path, content).map_err(|err| format!("write {} failed: {err}", path.display()))
}

pub(in crate::commands::sdk) fn validate_project_name(name: &str) -> Result<(), String> {
    if name.is_empty() {
        return Err(String::from("project name must not be empty"));
    }
    let valid = name
        .chars()
        .all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_');
    if !valid {
        return Err(format!(
            "invalid project name: {name} (expected [a-zA-Z0-9_-])"
        ));
    }
    Ok(())
}

pub(in crate::commands::sdk) fn is_supported_template(template: &str) -> bool {
    matches!(
        template,
        "local" | "network" | "mission" | "service" | "action"
    )
}

pub(in crate::commands::sdk) fn execute_sdk_migrate(
    project_dir: PathBuf,
    target_schema_version: u64,
    dry_run: bool,
) -> Result<SdkMigrateExecution, String> {
    if !project_dir.exists() {
        return Err(format!("project dir not found: {}", project_dir.display()));
    }
    if !project_dir.is_dir() {
        return Err(format!(
            "project path is not a directory: {}",
            project_dir.display()
        ));
    }

    let config_path = project_dir.join("robotrt.toml");
    if !config_path.exists() {
        return Err(format!("missing robotrt config: {}", config_path.display()));
    }

    let original = fs::read_to_string(&config_path)
        .map_err(|err| format!("read {} failed: {err}", config_path.display()))?;
    let from_schema_version = parse_schema_version(&original).unwrap_or(1);

    if target_schema_version < from_schema_version {
        return Err(format!(
            "target schema {} is lower than current schema {} (use sdk rollback for reverse operation)",
            target_schema_version, from_schema_version
        ));
    }
    if target_schema_version > SDK_LATEST_SCHEMA_VERSION {
        return Err(format!(
            "unsupported target schema {} (latest supported: {})",
            target_schema_version, SDK_LATEST_SCHEMA_VERSION
        ));
    }

    let fallback_project_name = project_dir
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or("robotrt_app")
        .to_string();
    let (migrated, applied_steps) = migrate_robotrt_config_chain(
        &original,
        from_schema_version,
        target_schema_version,
        &fallback_project_name,
    );

    let backup_path = config_path.with_file_name("robotrt.toml.bak");
    let manifest_path = project_dir.join("robotrt.migrate.manifest.json");
    let updated = migrated != original;
    if updated && !dry_run {
        fs::write(&backup_path, original)
            .map_err(|err| format!("write backup {} failed: {err}", backup_path.display()))?;
        fs::write(&config_path, &migrated).map_err(|err| {
            format!(
                "write migrated config {} failed: {err}",
                config_path.display()
            )
        })?;

        let manifest = serde_json::json!({
            "kind": "robotrt_sdk_migration_manifest",
            "project_dir": project_dir,
            "config_path": config_path,
            "backup_path": backup_path,
            "from_schema_version": from_schema_version,
            "to_schema_version": target_schema_version,
            "applied_steps": applied_steps,
        });
        fs::write(
            &manifest_path,
            serde_json::to_string_pretty(&manifest)
                .map_err(|err| format!("serialize migration manifest failed: {err}"))?,
        )
        .map_err(|err| {
            format!(
                "write migration manifest {} failed: {err}",
                manifest_path.display()
            )
        })?;
    }

    Ok(SdkMigrateExecution {
        project_dir,
        config_path,
        backup_path: if updated { Some(backup_path) } else { None },
        manifest_path: if updated && !dry_run {
            Some(manifest_path)
        } else {
            None
        },
        applied_steps,
        from_schema_version,
        to_schema_version: target_schema_version,
        updated,
        dry_run,
    })
}