use std::ffi::OsString;
use std::path::PathBuf;
use std::process::ExitCode;
use clap::{Parser, Subcommand};
mod add;
mod ci;
mod common;
mod download;
mod init;
mod install;
mod resolve;
mod upgrade;
pub(crate) type Res<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
#[derive(Parser)]
#[command(
name = "npm-utils",
version,
about = "Pure-Rust npm registry tools: install · ci · add · init · upgrade"
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Install {
#[arg(default_value = ".")]
dir: PathBuf,
},
Ci {
#[arg(default_value = ".")]
dir: PathBuf,
},
Add {
#[arg(required = true)]
packages: Vec<String>,
#[arg(long, default_value = ".")]
dir: PathBuf,
},
Init {
#[arg(long, default_value = ".")]
dir: PathBuf,
#[arg(long)]
name: Option<String>,
},
Upgrade {
packages: Vec<String>,
#[arg(long, default_value = ".")]
dir: PathBuf,
},
Resolve {
name: String,
#[arg(default_value = "*")]
range: String,
},
Download {
name: String,
#[arg(default_value = "*")]
range: String,
#[arg(long)]
out: Option<PathBuf>,
},
}
pub fn run(argv: impl IntoIterator<Item = OsString>) -> Res {
match Cli::parse_from(argv).command {
Command::Install { dir } => install::run(&dir),
Command::Ci { dir } => ci::run(&dir),
Command::Add { packages, dir } => add::run(&packages, &dir),
Command::Init { dir, name } => init::run(&dir, name.as_deref()),
Command::Upgrade { packages, dir } => upgrade::run(&packages, &dir),
Command::Resolve { name, range } => resolve::run(&name, &range),
Command::Download { name, range, out } => download::run(&name, &range, out.as_deref()),
}
}
pub fn main_with(argv: impl IntoIterator<Item = OsString>) -> ExitCode {
match run(argv) {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("npm-utils: {e}");
ExitCode::FAILURE
}
}
}
pub fn run_as_cargo_subcommand(argv: impl IntoIterator<Item = OsString>) -> ExitCode {
main_with(strip_cargo_prefix(argv.into_iter().collect()))
}
fn strip_cargo_prefix(mut args: Vec<OsString>) -> Vec<OsString> {
if args.get(1).is_some_and(|a| a == "npm-utils") {
args.remove(1);
}
args
}
#[cfg(test)]
mod tests {
use super::*;
fn osv(args: &[&str]) -> Vec<OsString> {
args.iter().map(OsString::from).collect()
}
#[test]
fn strip_cargo_prefix_drops_the_repassed_subcommand_name() {
assert_eq!(
strip_cargo_prefix(osv(&["cargo-npm-utils", "npm-utils", "add", "lit"])),
osv(&["cargo-npm-utils", "add", "lit"])
);
assert_eq!(
strip_cargo_prefix(osv(&["cargo-npm-utils", "install"])),
osv(&["cargo-npm-utils", "install"])
);
assert_eq!(
strip_cargo_prefix(osv(&["cargo-npm-utils"])),
osv(&["cargo-npm-utils"])
);
}
#[test]
fn cli_parses_the_verb_set() {
for argv in [
osv(&["npm-utils", "install"]),
osv(&["npm-utils", "ci", "/tmp/x"]),
osv(&["npm-utils", "add", "lit@^3", "--dir", "/tmp/x"]),
osv(&["npm-utils", "init", "--name", "demo"]),
osv(&["npm-utils", "upgrade"]),
osv(&["npm-utils", "resolve", "lit", "^3"]),
osv(&["npm-utils", "download", "ms", "--out", "/tmp/ms.tgz"]),
] {
assert!(Cli::try_parse_from(argv).is_ok());
}
assert!(Cli::try_parse_from(osv(&["npm-utils", "add"])).is_err());
}
}