solverforge-cli 1.1.3

CLI for scaffolding and managing SolverForge constraint solver projects
use std::fs;
use std::path::{Path, PathBuf};

pub const USE_PUBLISHED_DEPS_ENV: &str = "SF_USE_PUBLISHED_DEPS";

pub fn pin_generated_project_to_local_solverforge(project_dir: &Path) -> bool {
    if use_published_deps() {
        return false;
    }

    let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("CLI repo should have a workspace parent")
        .to_path_buf();
    let runtime_path = find_existing_path(&[workspace_root
        .join("solverforge-rs")
        .join("crates")
        .join("solverforge")]);
    let ui_path = find_existing_path(&[workspace_root.join("solverforge-ui")]);
    let maps_path = find_existing_path(&[workspace_root.join("solverforge-maps")]);

    let (Some(runtime_path), Some(ui_path)) = (runtime_path, ui_path) else {
        return false;
    };

    let cargo_toml_path = project_dir.join("Cargo.toml");
    let cargo_toml = fs::read_to_string(&cargo_toml_path).expect("read generated Cargo.toml");
    let runtime_line = format!(
        "solverforge = {{ path = \"{}\", features = [\"serde\", \"console\", \"verbose-logging\"] }}",
        toml_path(&runtime_path)
    );
    let ui_line = format!("solverforge-ui = {{ path = \"{}\" }}", toml_path(&ui_path));
    let maps_line = maps_path.map(|maps_path| {
        format!(
            "solverforge-maps = {{ path = \"{}\" }}",
            toml_path(&maps_path)
        )
    });

    let rewritten = cargo_toml
        .lines()
        .map(|line| {
            let trimmed = line.trim_start();
            if trimmed.starts_with("solverforge = ") {
                runtime_line.clone()
            } else if trimmed.starts_with("solverforge-ui = ") {
                ui_line.clone()
            } else if trimmed.starts_with("solverforge-maps = ") {
                maps_line.clone().unwrap_or_else(|| line.to_string())
            } else {
                line.to_string()
            }
        })
        .collect::<Vec<_>>()
        .join("\n");

    fs::write(cargo_toml_path, rewritten + "\n").expect("write pinned generated Cargo.toml");
    true
}

fn use_published_deps() -> bool {
    matches!(
        std::env::var(USE_PUBLISHED_DEPS_ENV),
        Ok(value) if matches!(value.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on")
    )
}

fn find_existing_path(candidates: &[PathBuf]) -> Option<PathBuf> {
    candidates.iter().find(|path| path.exists()).cloned()
}

fn toml_path(path: &Path) -> String {
    path.display().to_string().replace('\\', "/")
}