hni 0.0.2

ni-compatible package manager command router with node shim
Documentation
use std::path::Path;

use crate::core::{
    config::RunAgent,
    error::{HniError, HniResult},
    types::{Intent, PackageManager, ResolvedExecution},
};

use super::{
    context::ResolveContext,
    detect::detect_for_action,
    flags::{exclude_flag, normalize_ni_args, prepend},
    map::{
        add_command, execute_command, frozen_command, global_install_command,
        global_uninstall_command, install_command, run_command, uninstall_command, upgrade_command,
    },
};

pub fn resolve_ni(args: Vec<String>, ctx: &ResolveContext) -> HniResult<ResolvedExecution> {
    let use_global = args.iter().any(|arg| arg == "-g");
    let detected = detect_for_action(&ctx.cwd, &ctx.config, use_global)?;
    let args = normalize_ni_args(args, detected.pm);

    if use_global {
        let args = exclude_flag(args, "-g");
        return Ok(build_exec(
            detected.pm,
            Intent::Install,
            args,
            &ctx.cwd,
            true,
            detected.has_lock,
        ));
    }

    if args.iter().any(|a| a == "--frozen-if-present") {
        let args = exclude_flag(args, "--frozen-if-present");
        if detected.has_lock {
            return Ok(build_exec(
                detected.pm,
                Intent::CleanInstall,
                args,
                &ctx.cwd,
                false,
                true,
            ));
        }
        return Ok(build_exec(
            detected.pm,
            Intent::Install,
            args,
            &ctx.cwd,
            false,
            false,
        ));
    }

    if args.iter().any(|a| a == "--frozen") {
        let args = exclude_flag(args, "--frozen");
        return Ok(build_exec(
            detected.pm,
            Intent::CleanInstall,
            args,
            &ctx.cwd,
            false,
            true,
        ));
    }

    if args.is_empty() || args.iter().all(|a| a.starts_with('-')) {
        return Ok(build_exec(
            detected.pm,
            Intent::Install,
            args,
            &ctx.cwd,
            false,
            detected.has_lock,
        ));
    }

    Ok(build_exec(
        detected.pm,
        Intent::Add,
        args,
        &ctx.cwd,
        false,
        detected.has_lock,
    ))
}

pub fn resolve_nr(mut args: Vec<String>, ctx: &ResolveContext) -> HniResult<ResolvedExecution> {
    let detected = detect_for_action(&ctx.cwd, &ctx.config, false)?;

    if args.is_empty() {
        args.push("start".to_string());
    }

    let has_if_present = args.iter().any(|a| a == "--if-present");
    if has_if_present {
        args = exclude_flag(args, "--if-present");
    }

    if ctx.config.run_agent == RunAgent::Node {
        let mut node_args = vec!["--run".to_string()];
        node_args.extend(args);

        return Ok(ResolvedExecution {
            program: "node".to_string(),
            args: node_args,
            cwd: ctx.cwd.clone(),
            passthrough: true,
        });
    }

    let mut resolved = build_exec(
        detected.pm,
        Intent::Run,
        args,
        &ctx.cwd,
        false,
        detected.has_lock,
    );

    if has_if_present {
        if let Some(first) = resolved.args.first() {
            if matches!(first.as_str(), "run" | "task") {
                resolved.args.insert(1, "--if-present".to_string());
            } else {
                resolved.args.insert(0, "--if-present".to_string());
            }
        } else {
            resolved.args.push("--if-present".to_string());
        }
    }

    Ok(resolved)
}

pub fn resolve_nlx(args: Vec<String>, ctx: &ResolveContext) -> HniResult<ResolvedExecution> {
    let detected = detect_for_action(&ctx.cwd, &ctx.config, false)?;
    Ok(build_exec(
        detected.pm,
        Intent::Execute,
        args,
        &ctx.cwd,
        false,
        detected.has_lock,
    ))
}

pub fn resolve_nu(mut args: Vec<String>, ctx: &ResolveContext) -> HniResult<ResolvedExecution> {
    let detected = detect_for_action(&ctx.cwd, &ctx.config, false)?;
    let interactive = args
        .iter()
        .any(|a| matches!(a.as_str(), "-i" | "--interactive"));
    if interactive {
        args = exclude_flag(args, "-i");
        args = exclude_flag(args, "--interactive");
    }

    build_upgrade_exec(detected.pm, args, &ctx.cwd, interactive)
}

pub fn resolve_nun(args: Vec<String>, ctx: &ResolveContext) -> HniResult<ResolvedExecution> {
    let use_global = args.iter().any(|arg| arg == "-g");
    let detected = detect_for_action(&ctx.cwd, &ctx.config, use_global)?;
    let args = if use_global {
        exclude_flag(args, "-g")
    } else {
        args
    };

    if args.is_empty() {
        return Err(HniError::execution(
            "no dependencies selected for uninstall",
        ));
    }

    Ok(build_exec(
        detected.pm,
        Intent::Uninstall,
        args,
        &ctx.cwd,
        use_global,
        detected.has_lock,
    ))
}

pub fn resolve_nci(args: Vec<String>, ctx: &ResolveContext) -> HniResult<ResolvedExecution> {
    let detected = detect_for_action(&ctx.cwd, &ctx.config, false)?;

    if detected.has_lock {
        Ok(build_exec(
            detected.pm,
            Intent::CleanInstall,
            args,
            &ctx.cwd,
            false,
            true,
        ))
    } else {
        Ok(build_exec(
            detected.pm,
            Intent::Install,
            args,
            &ctx.cwd,
            false,
            false,
        ))
    }
}

pub fn resolve_na(args: Vec<String>, ctx: &ResolveContext) -> HniResult<ResolvedExecution> {
    let detected = detect_for_action(&ctx.cwd, &ctx.config, false)?;
    Ok(build_exec(
        detected.pm,
        Intent::AgentAlias,
        args,
        &ctx.cwd,
        false,
        detected.has_lock,
    ))
}

pub fn resolve_node_passthrough(args: Vec<String>, cwd: &Path) -> ResolvedExecution {
    ResolvedExecution {
        program: "node".to_string(),
        args,
        cwd: cwd.to_path_buf(),
        passthrough: true,
    }
}

pub fn resolve_node_routed(
    intent: Intent,
    args: Vec<String>,
    ctx: &ResolveContext,
) -> HniResult<ResolvedExecution> {
    match intent {
        Intent::Install => resolve_ni(args, ctx),
        Intent::Add | Intent::Execute => resolve_detected_intent(intent, args, ctx),
        Intent::Run => resolve_nr(args, ctx),
        Intent::Upgrade => resolve_nu(args, ctx),
        Intent::Uninstall => resolve_nun(args, ctx),
        Intent::CleanInstall => resolve_nci(args, ctx),
        Intent::AgentAlias => resolve_na(args, ctx),
        Intent::PassthroughNode => Ok(resolve_node_passthrough(args, &ctx.cwd)),
    }
}

fn resolve_detected_intent(
    intent: Intent,
    args: Vec<String>,
    ctx: &ResolveContext,
) -> HniResult<ResolvedExecution> {
    let detected = detect_for_action(&ctx.cwd, &ctx.config, false)?;
    Ok(build_exec(
        detected.pm,
        intent,
        args,
        &ctx.cwd,
        false,
        detected.has_lock,
    ))
}

fn build_upgrade_exec(
    pm: PackageManager,
    args: Vec<String>,
    cwd: &Path,
    interactive: bool,
) -> HniResult<ResolvedExecution> {
    let (program, args) = if interactive {
        match pm {
            PackageManager::Npm | PackageManager::Bun => {
                return Err(HniError::interactive(format!(
                    "interactive upgrade is not supported for {}",
                    pm.display_name()
                )));
            }
            PackageManager::Yarn => ("yarn".to_string(), prepend("upgrade-interactive", args)),
            PackageManager::YarnBerry => ("yarn".to_string(), prepend("up", prepend("-i", args))),
            PackageManager::Pnpm => ("pnpm".to_string(), prepend("update", prepend("-i", args))),
            PackageManager::Deno => (
                "deno".to_string(),
                prepend("outdated", prepend("--update", args)),
            ),
        }
    } else {
        upgrade_command(pm, args)
    };

    Ok(ResolvedExecution {
        program,
        args,
        cwd: cwd.to_path_buf(),
        passthrough: false,
    })
}

fn build_exec(
    pm: PackageManager,
    intent: Intent,
    args: Vec<String>,
    cwd: &Path,
    use_global: bool,
    has_lock: bool,
) -> ResolvedExecution {
    let (program, args) = match intent {
        Intent::Install => {
            if use_global {
                global_install_command(pm, args)
            } else {
                install_command(pm, args)
            }
        }
        Intent::Add => add_command(pm, args),
        Intent::Run => run_command(pm, args),
        Intent::Execute => execute_command(pm, args),
        Intent::Upgrade => upgrade_command(pm, args),
        Intent::Uninstall => {
            if use_global {
                global_uninstall_command(pm, args)
            } else {
                uninstall_command(pm, args)
            }
        }
        Intent::CleanInstall => {
            if has_lock {
                frozen_command(pm)
            } else {
                install_command(pm, args)
            }
        }
        Intent::AgentAlias => (pm.bin().to_string(), args),
        Intent::PassthroughNode => {
            return ResolvedExecution {
                program: "node".to_string(),
                args,
                cwd: cwd.to_path_buf(),
                passthrough: true,
            };
        }
    };

    ResolvedExecution {
        program,
        args,
        cwd: cwd.to_path_buf(),
        passthrough: false,
    }
}