use anyhow::Result;
use clap::Command;
use crate::{
commands,
core::{resolve::ResolveContext, types::InvocationKind},
};
pub use crate::core::types::HelpTopic;
use super::{
cli::command_parser,
help::{command_help, init_help},
};
pub type CommandHandler =
fn(Vec<String>, &ResolveContext) -> Result<Option<crate::core::types::ResolvedExecution>>;
#[derive(Clone, Copy)]
pub struct CommandSpec {
pub name: &'static str,
pub invocation: InvocationKind,
pub help_topic: HelpTopic,
pub about: &'static str,
pub long_about: &'static str,
pub examples: &'static str,
pub handler: CommandHandler,
}
const COMMAND_SPECS: &[CommandSpec] = &[
CommandSpec {
name: "ni",
invocation: InvocationKind::Ni,
help_topic: HelpTopic::Ni,
about: "install or add dependencies",
long_about: "Routes installs to the package manager detected from packageManager or lockfile.",
examples: "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",
handler: commands::handle_ni,
},
CommandSpec {
name: "nr",
invocation: InvocationKind::Nr,
help_topic: HelpTopic::Nr,
about: "run package scripts",
long_about: "Runs scripts in fast mode by default, then falls back to node or the detected package manager when needed.",
examples: "Examples:\n\
\n\
nr Run 'start'\n\
nr dev Run dev script\n\
nr --fast dev Force fast mode\n\
nr --pm dev Force package-manager mode\n\
nr test -- --watch Pass extra args to script\n\
nr --if-present lint Skip failure if script is missing",
handler: commands::handle_nr,
},
CommandSpec {
name: "nlx",
invocation: InvocationKind::Nlx,
help_topic: HelpTopic::Nlx,
about: "execute package binaries",
long_about: "Runs local or declared package binaries directly by default, then falls back to package-manager exec when needed.",
examples: "Examples:\n\
\n\
nlx --fast eslint .\n\
nlx vite@latest\n\
nlx eslint .\n\
nlx degit user/repo app",
handler: commands::handle_nlx,
},
CommandSpec {
name: "nru",
invocation: InvocationKind::Nru,
help_topic: HelpTopic::Nru,
about: "upgrade dependencies",
long_about: "Upgrades dependencies using package-manager-specific update commands.",
examples: "Examples:\n\
\n\
nru\n\
nru react react-dom\n\
nru --interactive Interactive mode when supported",
handler: commands::handle_nru,
},
CommandSpec {
name: "nun",
invocation: InvocationKind::Nun,
help_topic: HelpTopic::Nun,
about: "remove dependencies",
long_about: "Uninstalls dependencies. Supports interactive multi-select mode.",
examples: "Examples:\n\
\n\
nun lodash\n\
nun react react-dom\n\
nun --multi-select Interactive multi-select\n\
nun -g typescript",
handler: commands::handle_nun,
},
CommandSpec {
name: "nci",
invocation: InvocationKind::Nci,
help_topic: HelpTopic::Nci,
about: "clean install",
long_about: "Performs lockfile-clean install when lockfile exists; falls back to install otherwise.",
examples: "Examples:\n\
\n\
nci\n\
nci --prefer-offline",
handler: commands::handle_nci,
},
CommandSpec {
name: "na",
invocation: InvocationKind::Na,
help_topic: HelpTopic::Na,
about: "package manager alias",
long_about: "Forwards arguments directly to the detected package manager binary.",
examples: "Examples:\n\
\n\
na --version\n\
na config get registry\n\
na cache clean --force",
handler: commands::handle_na,
},
CommandSpec {
name: "np",
invocation: InvocationKind::Np,
help_topic: HelpTopic::Np,
about: "run shell commands in parallel",
long_about: "Runs each argument as a separate shell command concurrently. Returns first non-zero code.",
examples: "Examples:\n\
\n\
np \"npm:test\" \"npm:lint\"\n\
np \"echo one\" \"echo two\"",
handler: commands::handle_np,
},
CommandSpec {
name: "ns",
invocation: InvocationKind::Ns,
help_topic: HelpTopic::Ns,
about: "run shell commands sequentially",
long_about: "Runs each argument in order and stops at first failure.",
examples: "Examples:\n\
\n\
ns \"npm run build\" \"npm run test\"\n\
ns \"echo pre\" \"echo post\"",
handler: commands::handle_ns,
},
CommandSpec {
name: "node",
invocation: InvocationKind::NodeShim,
help_topic: HelpTopic::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.",
examples: "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",
handler: commands::handle_node,
},
];
pub fn command_specs() -> &'static [CommandSpec] {
COMMAND_SPECS
}
pub fn command_spec_by_name(name: &str) -> Option<&'static CommandSpec> {
command_specs().iter().find(|spec| spec.name == name)
}
pub fn command_spec_by_invocation(invocation: InvocationKind) -> Option<&'static CommandSpec> {
command_specs()
.iter()
.find(|spec| spec.invocation == invocation)
}
pub fn help_topic_by_name(name: &str) -> Option<HelpTopic> {
match name {
"hni" | "doctor" | "completion" | "help" => Some(HelpTopic::Hni),
"init" => Some(HelpTopic::Init),
_ => command_spec_by_name(name).map(|spec| spec.help_topic),
}
}
pub fn help_topic_for_invocation(invocation: InvocationKind) -> HelpTopic {
command_spec_by_invocation(invocation)
.map(|spec| spec.help_topic)
.unwrap_or(HelpTopic::Hni)
}
pub fn invocation_from_name(name: &str) -> Option<InvocationKind> {
command_spec_by_name(name).map(|spec| spec.invocation)
}
pub fn help_command_for_topic(topic: HelpTopic) -> Command {
match topic {
HelpTopic::Hni => super::help::top_level_help(),
HelpTopic::Init => init_help(),
_ => {
let spec = command_specs()
.iter()
.find(|spec| spec.help_topic == topic)
.expect("help topic should have matching command spec");
command_help(spec)
}
}
}
pub fn command_subcommands() -> impl Iterator<Item = Command> {
command_specs()
.iter()
.map(|spec| command_parser(spec.name).about(spec.about))
}