zacor 0.1.0

Package manager and dispatcher for zr — install, manage, and run modular CLI packages
Documentation
use crate::error::*;
use crate::{package_definition, paths, receipt};
use std::path::Path;

pub fn run(home: &Path, platforms: &[String]) -> Result<()> {
    let cwd = std::env::current_dir().context("failed to get current directory")?;

    // Resolve platforms: explicit args or auto-detect
    let resolved = if platforms.is_empty() {
        let detected = detect_platforms(&cwd);
        if detected.is_empty() {
            bail!(
                "No platforms detected. Specify platforms explicitly:\n  \
                 zacor init claude-code gemini\n\n\
                 Valid platforms: {}",
                zacor_package::skills::PLATFORMS
                    .iter()
                    .map(|p| p.name)
                    .collect::<Vec<_>>()
                    .join(", ")
            );
        }
        eprintln!(
            "Auto-detected platforms: {}",
            detected.join(", ")
        );
        detected
    } else {
        zacor_package::skills::validate_platforms(platforms)
            .map_err(|e| anyhow::anyhow!("{e}"))?;
        platforms.to_vec()
    };

    // Create .zr/ project root
    let zr_dir = cwd.join(".zr");
    if !zr_dir.exists() {
        std::fs::create_dir_all(&zr_dir).context("failed to create .zr/")?;
        eprintln!("Created .zr/");
    }

    // Scan installed packages for those with an `init` command
    let packages = receipt::list_all(home).context("failed to list packages")?;
    let platforms_csv = resolved.join(",");
    let mut dispatched = 0;
    let mut failed = 0;

    for (name, r) in &packages {
        if !r.active {
            continue;
        }
        let def_path = paths::definition_path(home, name, &r.current);
        let def = match package_definition::parse_file(&def_path) {
            Ok(d) => d,
            Err(_) => continue,
        };

        if !def.commands.contains_key("init") {
            continue;
        }

        eprint!("  {name}... ");
        let status = std::process::Command::new("zr")
            .args([name.as_str(), "init", &platforms_csv])
            .status();

        match status {
            Ok(s) if s.success() => {
                eprintln!("ok");
                dispatched += 1;
            }
            Ok(s) => {
                eprintln!("failed (exit {})", s.code().unwrap_or(-1));
                failed += 1;
            }
            Err(e) => {
                eprintln!("failed ({e})");
                failed += 1;
            }
        }
    }

    eprintln!(
        "\nPlatforms: [{}]\nPackages dispatched: {dispatched}",
        resolved.join(", "),
    );
    if failed > 0 {
        bail!("{failed} package(s) failed during init");
    }
    Ok(())
}

fn detect_platforms(root: &Path) -> Vec<String> {
    zacor_package::skills::PLATFORMS
        .iter()
        .filter(|p| root.join(p.dir).is_dir())
        .map(|p| p.name.to_string())
        .collect()
}