hni 0.0.2

ni-compatible package manager command router with node shim
Documentation
use clap::{Arg, ArgAction, Command, builder::PossibleValuesParser, value_parser};

use crate::app::init::SUPPORTED_SHELL_NAMES;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HelpTopic {
    Hni,
    Ni,
    Nr,
    Nlx,
    Nu,
    Nun,
    Nci,
    Na,
    Np,
    Ns,
    Node,
    Init,
}

pub fn print_help(topic: HelpTopic) {
    let mut cmd = help_command(topic);
    let _ = cmd.print_long_help();
    println!();
}

pub fn help_command(topic: HelpTopic) -> Command {
    match topic {
        HelpTopic::Hni => top_level_help(),
        HelpTopic::Ni => command_help(
            "ni",
            "install or add dependencies",
            "Routes installs to the package manager detected from packageManager or lockfile.",
            "Examples:\n\
             \n\
             ni                   Install dependencies\n\
             ni vite              Add dependency\n\
             ni -D vitest         Add dev dependency\n\
             ni --interactive     Search and choose a package interactively\n\
             ni --frozen          Use lockfile-only install (nci behavior)\n\
             ni -- --help         Forward --help to underlying package manager\n\
             ni -g npm-check-updates",
        ),
        HelpTopic::Nr => command_help(
            "nr",
            "run package scripts",
            "Runs scripts through the detected package manager and keeps npm '--' behavior consistent.",
            "Examples:\n\
             \n\
             nr                   Run 'start'\n\
             nr dev               Run dev script\n\
             nr test -- --watch   Pass extra args to script\n\
             nr --if-present lint Skip failure if script is missing\n\
             nr --repeat-last      Re-run last script",
        ),
        HelpTopic::Nlx => command_help(
            "nlx",
            "execute package binaries",
            "Runs package binaries without permanently installing them (npx / pnpm dlx / yarn dlx / bun x).",
            "Examples:\n\
             \n\
             nlx vite@latest\n\
             nlx eslint .\n\
             nlx degit user/repo app",
        ),
        HelpTopic::Nu => command_help(
            "nu",
            "upgrade dependencies",
            "Upgrades dependencies using package-manager-specific update commands.",
            "Examples:\n\
             \n\
             nu\n\
             nu react react-dom\n\
             nu --interactive      Interactive mode when supported",
        ),
        HelpTopic::Nun => command_help(
            "nun",
            "remove dependencies",
            "Uninstalls dependencies. Supports interactive multi-select mode.",
            "Examples:\n\
             \n\
             nun lodash\n\
             nun react react-dom\n\
             nun --multi-select    Interactive multi-select\n\
             nun -g typescript",
        ),
        HelpTopic::Nci => command_help(
            "nci",
            "clean install",
            "Performs lockfile-clean install when lockfile exists; falls back to install otherwise.",
            "Examples:\n\
             \n\
             nci\n\
             nci --prefer-offline",
        ),
        HelpTopic::Na => command_help(
            "na",
            "package manager alias",
            "Forwards arguments directly to the detected package manager binary.",
            "Examples:\n\
             \n\
             na --version\n\
             na config get registry\n\
             na cache clean --force",
        ),
        HelpTopic::Np => command_help(
            "np",
            "run shell commands in parallel",
            "Runs each argument as a separate shell command concurrently. Returns first non-zero code.",
            "Examples:\n\
             \n\
             np \"npm:test\" \"npm:lint\"\n\
             np \"echo one\" \"echo two\"",
        ),
        HelpTopic::Ns => command_help(
            "ns",
            "run shell commands sequentially",
            "Runs each argument in order and stops at first failure.",
            "Examples:\n\
             \n\
             ns \"npm run build\" \"npm run test\"\n\
             ns \"echo pre\" \"echo post\"",
        ),
        HelpTopic::Node => with_global_flags(
            Command::new("node")
                .about("package-manager-aware node shim")
                .long_about(
                    "Interprets npm-like verbs and routes them through hni command resolution.\n\
                     Non-routed invocations pass through to the real Node.js binary.",
                )
                .arg(command_args_arg())
                .after_help(
                    "Passthrough examples:\n\
                     \n\
                     node script.js\n\
                     node -v\n\
                     node -- --trace-warnings\n\
                     \n\
                     Routed examples:\n\
                     \n\
                     node install vite\n\
                     node run dev -- --port=3000\n\
                     node p \"echo one\" \"echo two\"\n\
                     \n\
                     Routed verbs: p, s, install|i, add, run, exec|x|dlx, update|upgrade, uninstall|remove, ci",
                ),
        ),
        HelpTopic::Init => with_global_flags(
            Command::new("init")
                .about("print shell init code for node shim")
                .long_about(
                    "Prints shell-specific init code that captures the current real Node.js binary\n\
                     and registers a shell-level node wrapper for shim behavior.\n\
                     Add the generated line at the end of your shell config, after nvm/mise/asdf/fnm/volta init.",
                )
                .arg(init_shell_arg())
                .after_help(
                    "Examples:\n\
                     \n\
                     hni init bash\n\
                     hni init zsh\n\
                     hni init fish\n\
                     hni init powershell\n\
                     hni init nushell",
                ),
        ),
    }
}

fn top_level_help() -> Command {
    with_global_flags(
        Command::new("hni")
            .about("use the right package manager")
            .long_about(
                "hni is a multicall package-manager router.\n\
                 It powers commands like ni, nr, nlx, nu, nun, nci, na, np, ns, and node.",
            )
            .subcommand(Command::new("ni").about("install or add dependencies"))
            .subcommand(Command::new("nr").about("run package scripts"))
            .subcommand(Command::new("nlx").about("execute package binaries"))
            .subcommand(Command::new("nu").about("upgrade dependencies"))
            .subcommand(Command::new("nun").about("remove dependencies"))
            .subcommand(Command::new("nci").about("clean install"))
            .subcommand(Command::new("na").about("package manager alias"))
            .subcommand(Command::new("np").about("run shell commands in parallel"))
            .subcommand(Command::new("ns").about("run shell commands sequentially"))
            .subcommand(Command::new("node").about("package-manager-aware node shim"))
            .subcommand(Command::new("init").about("print shell init code"))
            .subcommand(Command::new("doctor").about("print environment and detection diagnostics"))
            .subcommand(Command::new("completion").about("print shell completion script"))
            .after_help(
                "Quick examples:\n\
                 \n\
                 ni vite\n\
                 ni --explain react -D\n\
                 nr dev -- --port=3000\n\
                 nlx create-vite@latest\n\
                 nu --interactive\n\
                 nun --multi-select\n\
                 np \"echo one\" \"echo two\"\n\
                 ns \"npm run build\" \"npm run test\"\n\
                 hni init bash\n\
                 hni doctor\n\
                 hni help ni\n\
                 hni completion zsh\n\
                 node install react",
            ),
    )
}

fn command_help(
    name: &'static str,
    about: &'static str,
    long_about: &'static str,
    examples: &'static str,
) -> Command {
    with_global_flags(
        Command::new(name)
            .about(about)
            .long_about(long_about)
            .arg(command_args_arg())
            .after_help(examples),
    )
}

fn with_global_flags(cmd: Command) -> Command {
    cmd.disable_help_flag(true)
        .disable_version_flag(true)
        .arg(
            Arg::new("debug")
                .short('?')
                .long("debug-resolved")
                .help("print resolved command and exit (aliases: --dry-run, --print-command)")
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new("explain")
                .long("explain")
                .help("print detection + resolution details and exit")
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new("cwd")
                .short('C')
                .value_name("DIR")
                .help("run as if in <dir>")
                .value_parser(value_parser!(std::path::PathBuf))
                .action(ArgAction::Append),
        )
        .arg(
            Arg::new("version")
                .short('v')
                .long("version")
                .help("show versions")
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new("help")
                .short('h')
                .long("help")
                .help("show help")
                .action(ArgAction::SetTrue),
        )
}

fn command_args_arg() -> Arg {
    Arg::new("args")
        .value_name("ARGS")
        .help("arguments forwarded to the resolved command")
        .num_args(0..)
        .allow_hyphen_values(true)
        .action(ArgAction::Append)
}

fn init_shell_arg() -> Arg {
    Arg::new("shell")
        .value_name("SHELL")
        .help("shell to initialize")
        .required(true)
        .value_parser(PossibleValuesParser::new(SUPPORTED_SHELL_NAMES))
}