hni 0.0.3

ni-compatible package manager command router with node shim
Documentation
use std::{
    fs,
    path::{Path, PathBuf},
    process::Command,
};

mod support;

#[test]
fn multicall_aliases_resolve_expected_commands() {
    support::with_env_lock(|| {
        let work = tempfile::tempdir().unwrap();
        let fixtures = work.path().join("fixtures");
        fs::create_dir_all(&fixtures).unwrap();

        let npm_proj = fixtures.join("npm");
        fs::create_dir_all(&npm_proj).unwrap();
        fs::write(
            npm_proj.join("package.json"),
            r#"{"name":"x","scripts":{"dev":"vite"}}"#,
        )
        .unwrap();
        fs::write(npm_proj.join("package-lock.json"), "lock").unwrap();

        let bin_dir = work.path().join("bin");
        fs::create_dir_all(&bin_dir).unwrap();

        let exe = hni_executable_path();
        if !exe.exists() {
            return;
        }
        create_alias(&exe, &bin_dir, "ni");
        create_alias(&exe, &bin_dir, "nr");
        create_alias(&exe, &bin_dir, "nlx");
        create_alias(&exe, &bin_dir, "nru");
        create_alias(&exe, &bin_dir, "nun");
        create_alias(&exe, &bin_dir, "nci");
        create_alias(&exe, &bin_dir, "np");
        create_alias(&exe, &bin_dir, "ns");
        create_alias(&exe, &bin_dir, "node");

        let ni_out = run_alias(
            &bin_dir,
            "ni",
            vec!["-C", npm_proj.to_str().unwrap(), "vite", "?"],
            &[],
        );
        assert_eq!(ni_out.trim(), "npm i vite");

        let nr_out = run_alias(
            &bin_dir,
            "nr",
            vec![
                "-C",
                npm_proj.to_str().unwrap(),
                "--pm",
                "dev",
                "--port=3000",
                "?",
            ],
            &[],
        );
        assert_eq!(nr_out.trim(), "npm run dev -- --port=3000");

        let nr_if_present_out = run_alias(
            &bin_dir,
            "nr",
            vec![
                "-C",
                npm_proj.to_str().unwrap(),
                "--pm",
                "--if-present",
                "missing-script",
                "?",
            ],
            &[],
        );
        assert_eq!(
            nr_if_present_out.trim(),
            "npm run --if-present missing-script"
        );

        let ni_frozen_if_present_out = run_alias(
            &bin_dir,
            "ni",
            vec!["-C", npm_proj.to_str().unwrap(), "--frozen-if-present", "?"],
            &[],
        );
        assert_eq!(ni_frozen_if_present_out.trim(), "npm ci");

        let ni_global_out = run_alias(
            &bin_dir,
            "ni",
            vec!["-C", npm_proj.to_str().unwrap(), "-g", "eslint", "?"],
            &[("HNI_GLOBAL_PACKAGE_MANAGER", "yarn")],
        );
        assert_eq!(ni_global_out.trim(), "yarn global add eslint");

        let node_out = run_alias(
            &bin_dir,
            "node",
            vec!["-C", npm_proj.to_str().unwrap(), "--pm", "run", "dev", "?"],
            &[],
        );
        assert_eq!(node_out.trim(), "npm run dev");

        let nlx_out = run_alias(
            &bin_dir,
            "nlx",
            vec![
                "-C",
                npm_proj.to_str().unwrap(),
                "--pm",
                "--debug-resolved",
                "vitest",
                "--",
                "--help",
            ],
            &[],
        );
        assert!(
            nlx_out.trim().contains("npx vitest -- --help"),
            "unexpected nlx debug output: {}",
            nlx_out.trim()
        );

        let nru_out = run_alias(
            &bin_dir,
            "nru",
            vec!["-C", npm_proj.to_str().unwrap(), "vite", "?"],
            &[],
        );
        assert_eq!(nru_out.trim(), "npm update vite");

        let nci_out = run_alias(
            &bin_dir,
            "nci",
            vec!["-C", npm_proj.to_str().unwrap(), "?"],
            &[],
        );
        assert_eq!(nci_out.trim(), "npm ci");

        let nun_global_out = run_alias(
            &bin_dir,
            "nun",
            vec!["-C", npm_proj.to_str().unwrap(), "-g", "eslint", "?"],
            &[("HNI_GLOBAL_PACKAGE_MANAGER", "yarn")],
        );
        assert_eq!(nun_global_out.trim(), "yarn global remove eslint");

        let np_out = run_alias(
            &bin_dir,
            "np",
            vec![
                "-C",
                npm_proj.to_str().unwrap(),
                "echo one",
                "echo two",
                "?",
            ],
            &[],
        );
        assert_eq!(
            np_out.trim(),
            "hni batch:parallel \"echo one\" \"echo two\""
        );

        let ns_out = run_alias(
            &bin_dir,
            "ns",
            vec![
                "-C",
                npm_proj.to_str().unwrap(),
                "echo one",
                "echo two",
                "?",
            ],
            &[],
        );
        assert_eq!(
            ns_out.trim(),
            "hni batch:sequential \"echo one\" \"echo two\""
        );

        let node_parallel_out = run_alias(
            &bin_dir,
            "node",
            vec![
                "-C",
                npm_proj.to_str().unwrap(),
                "p",
                "echo one",
                "echo two",
                "?",
            ],
            &[],
        );
        assert_eq!(
            node_parallel_out.trim(),
            "hni batch:parallel \"echo one\" \"echo two\""
        );

        let node_sequential_out = run_alias(
            &bin_dir,
            "node",
            vec![
                "-C",
                npm_proj.to_str().unwrap(),
                "s",
                "echo one",
                "echo two",
                "?",
            ],
            &[],
        );
        assert_eq!(
            node_sequential_out.trim(),
            "hni batch:sequential \"echo one\" \"echo two\""
        );

        let fake_node = work.path().join(if cfg!(windows) {
            "real-node.exe"
        } else {
            "real-node"
        });
        fs::write(&fake_node, "#!/bin/sh\nexit 0\n").unwrap();
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mut perms = fs::metadata(&fake_node).unwrap().permissions();
            perms.set_mode(0o755);
            fs::set_permissions(&fake_node, perms).unwrap();
        }

        let passthrough_out = run_alias(
            &bin_dir,
            "node",
            vec!["script.js", "?"],
            &[("HNI_REAL_NODE", fake_node.to_str().unwrap())],
        );
        let output = passthrough_out.trim();
        assert!(output.contains(fake_node.to_string_lossy().as_ref()));
        assert!(output.contains("script.js"));

        let node_flag_out = run_alias(
            &bin_dir,
            "node",
            vec!["-p", "1+1", "?"],
            &[("HNI_REAL_NODE", fake_node.to_str().unwrap())],
        );
        let output = node_flag_out.trim();
        assert!(output.contains(fake_node.to_string_lossy().as_ref()));
        assert!(output.contains("-p"));
        assert!(output.contains("1+1"));
    });
}

fn hni_executable_path() -> PathBuf {
    if let Ok(path) = std::env::var("CARGO_BIN_EXE_hni") {
        return PathBuf::from(path);
    }

    let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    path.push("target");
    path.push("debug");
    path.push(if cfg!(windows) { "hni.exe" } else { "hni" });
    path
}

fn run_alias(bin_dir: &Path, alias: &str, args: Vec<&str>, extra_env: &[(&str, &str)]) -> String {
    let alias_bin = if cfg!(windows) {
        format!("{alias}.exe")
    } else {
        alias.to_string()
    };

    let mut cmd = Command::new(bin_dir.join(alias_bin));
    cmd.args(args).env("HNI_SKIP_PM_CHECK", "1");

    for (key, value) in extra_env {
        cmd.env(key, value);
    }

    let output = cmd.output().expect("failed to run alias binary");
    assert!(
        output.status.success(),
        "command failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    String::from_utf8_lossy(&output.stdout).to_string()
}

fn create_alias(target: &Path, dir: &Path, alias: &str) {
    let alias_path = if cfg!(windows) {
        dir.join(format!("{alias}.exe"))
    } else {
        dir.join(alias)
    };

    if alias_path.exists() {
        fs::remove_file(&alias_path).unwrap();
    }

    #[cfg(unix)]
    {
        std::os::unix::fs::symlink(target, alias_path).unwrap();
    }

    #[cfg(windows)]
    {
        fs::copy(target, alias_path).unwrap();
    }
}