hni 0.0.2

ni-compatible package manager command router with node shim
Documentation
use crate::{
    core::{
        batch::{self, BatchMode},
        error::HniResult,
        resolve::{self, ResolveContext},
        types::{Intent, NodeShimDecision, NodeShimMode, ResolvedExecution},
    },
    platform::node::{NODE_SHIM_ENV, SHIM_ACTIVE_ENV},
};

pub fn handle(args: Vec<String>, ctx: &ResolveContext) -> HniResult<Option<ResolvedExecution>> {
    let (decision, routed_args) = decide(&args);

    let resolved = match decision.mode {
        NodeShimMode::PassthroughNode => resolve::resolve_node_passthrough(routed_args, &ctx.cwd),
        NodeShimMode::RouteToIntent(intent) => {
            resolve::resolve_node_routed(intent, routed_args, ctx)?
        }
        NodeShimMode::RunParallel => {
            batch::make_execution(BatchMode::Parallel, routed_args, &ctx.cwd)
        }
        NodeShimMode::RunSequential => {
            batch::make_execution(BatchMode::Sequential, routed_args, &ctx.cwd)
        }
    };

    Ok(Some(resolved))
}

pub fn decide(args: &[String]) -> (NodeShimDecision, Vec<String>) {
    let shim_active = std::env::var_os(SHIM_ACTIVE_ENV).is_some();
    let shim_disabled = std::env::var_os(NODE_SHIM_ENV)
        .and_then(|value| value.into_string().ok())
        .is_some_and(|value| node_shim_disabled(&value));
    decide_with_shim_state(args, shim_active, shim_disabled)
}

pub fn decide_with_shim_state(
    args: &[String],
    shim_active: bool,
    shim_disabled: bool,
) -> (NodeShimDecision, Vec<String>) {
    if shim_disabled {
        return passthrough(
            args.to_vec(),
            "node shim disabled by HNI_NODE environment override",
        );
    }

    if shim_active {
        return passthrough(args.to_vec(), "shim recursion guard active");
    }

    let Some((first, rest)) = args.split_first() else {
        return passthrough(Vec::new(), "node with no args should open REPL");
    };

    if first == "--" {
        return passthrough(
            rest.to_vec(),
            "double-dash explicitly requests raw node passthrough",
        );
    }

    if first.starts_with('-') {
        return passthrough(
            args.to_vec(),
            "flag-first invocation should passthrough to real node",
        );
    }

    let verb = first.to_ascii_lowercase();
    let routed_args = rest.to_vec();

    match verb.as_str() {
        "p" => decision(
            NodeShimMode::RunParallel,
            routed_args,
            "route p through batch parallel",
        ),
        "s" => decision(
            NodeShimMode::RunSequential,
            routed_args,
            "route s through batch sequential",
        ),
        "install" | "i" => route(
            Intent::Install,
            routed_args,
            "route install through ni parser",
        ),
        "add" => route(Intent::Add, routed_args, "route add to package manager add"),
        "run" => route(Intent::Run, routed_args, "route run through nr parser"),
        "exec" | "x" | "dlx" => route(Intent::Execute, routed_args, "route exec through nlx"),
        "update" | "upgrade" => route(Intent::Upgrade, routed_args, "route update through nu"),
        "uninstall" | "remove" => route(
            Intent::Uninstall,
            routed_args,
            "route uninstall through nun",
        ),
        "ci" => route(Intent::CleanInstall, routed_args, "route ci through nci"),
        _ => passthrough(args.to_vec(), "unknown verb: passthrough to real node"),
    }
}

fn route(intent: Intent, args: Vec<String>, reason: &str) -> (NodeShimDecision, Vec<String>) {
    decision(NodeShimMode::RouteToIntent(intent), args, reason)
}

fn passthrough(args: Vec<String>, reason: &str) -> (NodeShimDecision, Vec<String>) {
    decision(NodeShimMode::PassthroughNode, args, reason)
}

fn decision(
    mode: NodeShimMode,
    args: Vec<String>,
    reason: &str,
) -> (NodeShimDecision, Vec<String>) {
    (
        NodeShimDecision {
            mode,
            reason: reason.to_string(),
        },
        args,
    )
}

fn node_shim_disabled(value: &str) -> bool {
    matches!(
        value.trim().to_ascii_lowercase().as_str(),
        "0" | "off" | "false" | "disable" | "disabled"
    )
}

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

    #[test]
    fn routes_install() {
        let (decision, args) = decide(&["install".into(), "vite".into()]);
        assert_eq!(args, vec!["vite"]);
        assert!(matches!(
            decision.mode,
            NodeShimMode::RouteToIntent(Intent::Install)
        ));
    }

    #[test]
    fn passthrough_unknown() {
        let (decision, args) = decide(&["server.js".into()]);
        assert_eq!(args, vec!["server.js"]);
        assert!(matches!(decision.mode, NodeShimMode::PassthroughNode));
    }

    #[test]
    fn env_override_disables_shim() {
        let (decision, args) = decide_with_shim_state(&["run".into(), "dev".into()], false, true);
        assert_eq!(args, vec!["run", "dev"]);
        assert!(matches!(decision.mode, NodeShimMode::PassthroughNode));
    }

    #[test]
    fn passthrough_double_dash() {
        let (decision, args) = decide(&["--".into(), "-v".into()]);
        assert_eq!(args, vec!["-v"]);
        assert!(matches!(decision.mode, NodeShimMode::PassthroughNode));
    }

    #[test]
    fn routes_parallel_short_verb() {
        let (decision, args) = decide(&["p".into(), "echo hi".into()]);
        assert_eq!(args, vec!["echo hi"]);
        assert!(matches!(decision.mode, NodeShimMode::RunParallel));
    }

    #[test]
    fn routes_sequential_short_verb() {
        let (decision, args) = decide(&["s".into(), "echo hi".into()]);
        assert_eq!(args, vec!["echo hi"]);
        assert!(matches!(decision.mode, NodeShimMode::RunSequential));
    }

    #[test]
    fn passthrough_flag_first() {
        let (decision, args) = decide(&["-p".into(), "1+1".into()]);
        assert_eq!(args, vec!["-p", "1+1"]);
        assert!(matches!(decision.mode, NodeShimMode::PassthroughNode));
    }

    #[test]
    fn routes_exec_aliases() {
        for verb in ["exec", "x", "dlx"] {
            let (decision, args) = decide(&[verb.into(), "vitest".into()]);
            assert_eq!(args, vec!["vitest"]);
            assert!(matches!(
                decision.mode,
                NodeShimMode::RouteToIntent(Intent::Execute)
            ));
        }
    }

    #[test]
    fn routes_upgrade_aliases() {
        for verb in ["update", "upgrade"] {
            let (decision, args) = decide(&[verb.into(), "vite".into()]);
            assert_eq!(args, vec!["vite"]);
            assert!(matches!(
                decision.mode,
                NodeShimMode::RouteToIntent(Intent::Upgrade)
            ));
        }
    }

    #[test]
    fn routes_uninstall_aliases() {
        for verb in ["uninstall", "remove"] {
            let (decision, args) = decide(&[verb.into(), "vite".into()]);
            assert_eq!(args, vec!["vite"]);
            assert!(matches!(
                decision.mode,
                NodeShimMode::RouteToIntent(Intent::Uninstall)
            ));
        }
    }

    #[test]
    fn routes_ci_to_clean_install() {
        let (decision, args) = decide(&["ci".into()]);
        assert_eq!(args, Vec::<String>::new());
        assert!(matches!(
            decision.mode,
            NodeShimMode::RouteToIntent(Intent::CleanInstall)
        ));
    }

    #[test]
    fn routes_verbs_case_insensitively() {
        let (decision, args) = decide(&["RUN".into(), "dev".into()]);
        assert_eq!(args, vec!["dev"]);
        assert!(matches!(
            decision.mode,
            NodeShimMode::RouteToIntent(Intent::Run)
        ));
    }

    #[test]
    fn passthrough_when_no_args() {
        let (decision, args) = decide(&[]);
        assert_eq!(args, Vec::<String>::new());
        assert!(matches!(decision.mode, NodeShimMode::PassthroughNode));
    }

    #[test]
    fn disabled_values_are_recognized_case_insensitively() {
        for value in ["0", "off", "OFF", "false", "False", "disable", "disabled"] {
            assert!(node_shim_disabled(value), "{value} should disable the shim");
        }

        for value in ["1", "on", "true", "npm"] {
            assert!(
                !node_shim_disabled(value),
                "{value} should not disable the shim"
            );
        }
    }
}