clankers-cli 0.1.2

Command-line interface for clankeRS
use std::fs;
use std::path::{Component, Path, PathBuf};

use anyhow::{bail, Context, Result};

pub fn execute(name: &str, template: &str) -> Result<()> {
    let template_dir = find_template(template)?;
    let dest = Path::new(name);

    if dest.exists() {
        bail!("directory '{}' already exists", name);
    }

    copy_dir_recursive(&template_dir, dest)?;
    replace_in_tree(dest, "{{PROJECT_NAME}}", name)?;

    let clankers_dep = clankers_dependency_line(dest)?;
    replace_in_tree(dest, "{{CLANKERS_DEPENDENCY}}", &clankers_dep)?;

    println!("clankeRS project created: {name}\n\nNext steps:\n  cd {name}\n  clankers run");
    Ok(())
}

fn clankers_dependency_line(dest: &Path) -> Result<String> {
    if let Some(workspace) = find_workspace_root() {
        let clankers_crate = workspace.join("crates/clankers");
        if clankers_crate.exists() {
            let dest_abs = std::env::current_dir()?.join(dest);
            if let Some(rel) = relative_path(&dest_abs, &clankers_crate) {
                return Ok(format!(
                    "clankers = {{ path = \"{}\" }}",
                    rel.to_string_lossy()
                ));
            }
        }
    }
    // Published crates.io install — no repo checkout required
    Ok(r#"clankers = "0.1""#.to_string())
}

fn relative_path(from: &Path, to: &Path) -> Option<PathBuf> {
    let from = from.components().collect::<Vec<_>>();
    let to = to.components().collect::<Vec<_>>();

    let mut i = 0;
    while i < from.len() && i < to.len() && from[i] == to[i] {
        i += 1;
    }

    let mut rel = PathBuf::new();
    for _ in i..from.len() {
        if matches!(from[i], Component::Normal(_)) {
            rel.push("..");
        }
    }
    for part in &to[i..] {
        rel.push(part.as_os_str());
    }
    if rel.as_os_str().is_empty() {
        rel.push(".");
    }
    Some(rel)
}

fn find_workspace_root() -> Option<PathBuf> {
    let cwd = std::env::current_dir().ok()?;
    for ancestor in cwd.ancestors() {
        if ancestor.join("crates/clankers").exists() {
            return Some(ancestor.to_path_buf());
        }
    }
    None
}

fn find_template(template: &str) -> Result<std::path::PathBuf> {
    let normalized = template.replace('-', "_");
    let bundled = PathBuf::from(env!("TEMPLATES_DIR")).join(&normalized);
    if bundled.exists() {
        return Ok(bundled);
    }

    let candidates = [
        format!("templates/{normalized}"),
        format!("../templates/{normalized}"),
        format!("../../templates/{normalized}"),
    ];

    for c in &candidates {
        let p = Path::new(c);
        if p.exists() {
            return Ok(p.canonicalize().unwrap_or_else(|_| p.to_path_buf()));
        }
    }

    if let Ok(cwd) = std::env::current_dir() {
        for ancestor in cwd.ancestors() {
            let p = ancestor.join("templates").join(&normalized);
            if p.exists() {
                return Ok(p);
            }
        }
    }

    bail!(
        "template '{template}' not found; available: basic_node, perception_node, ml_inference_node, replay_test_node"
    )
}

fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
    fs::create_dir_all(dst)?;
    for entry in fs::read_dir(src)? {
        let entry = entry?;
        let ty = entry.file_type()?;
        let dest_path = dst.join(entry.file_name());
        if ty.is_dir() {
            copy_dir_recursive(&entry.path(), &dest_path)?;
        } else {
            fs::copy(entry.path(), &dest_path)?;
        }
    }
    Ok(())
}

fn replace_in_tree(dir: &Path, from: &str, to: &str) -> Result<()> {
    for entry in walkdir::WalkDir::new(dir) {
        let entry = entry?;
        if entry.file_type().is_file() {
            let path = entry.path();
            let content = fs::read_to_string(path).context("read template file")?;
            if content.contains(from) {
                fs::write(path, content.replace(from, to))?;
            }
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn relative_path_to_sibling_crate() {
        let from = Path::new("/workspace/clankeRS/hello_clanker");
        let to = Path::new("/workspace/clankeRS/crates/clankers");
        assert_eq!(
            relative_path(from, to).unwrap(),
            PathBuf::from("../crates/clankers")
        );
    }
}