mod cli_args;
mod commands;
mod dep_chain;
mod deprecations;
mod dirs;
mod engines;
mod patches;
mod pnpmfile;
mod progress;
mod state;
mod update_check;
mod version;
#[cfg(all(feature = "mimalloc", not(debug_assertions)))]
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
use clap::{Parser, Subcommand, ValueEnum};
use miette::{Context, IntoDiagnostic, miette};
use std::ffi::OsString;
use std::path::PathBuf;
use tracing_subscriber::prelude::*;
fn extract_config_overrides(args: &mut Vec<OsString>) -> Vec<(String, String)> {
let mut out = Vec::new();
let mut i = 1;
while i < args.len() {
let Some(s) = args[i].to_str() else {
i += 1;
continue;
};
if s == "--" {
break;
}
if let Some(rest) = s.strip_prefix("--config.") {
let (key, value) = match rest.split_once('=') {
Some((k, v)) => (k.to_string(), v.to_string()),
None => (rest.to_string(), "true".to_string()),
};
if !key.is_empty() {
out.push((key, value));
args.remove(i);
continue;
}
}
i += 1;
}
out
}
fn rewrite_multicall_argv(mut args: Vec<OsString>) -> Vec<OsString> {
normalize_npm_interpreter_shim_argv(&mut args);
let Some(argv0) = args.first() else {
return args;
};
let stem = std::path::Path::new(argv0)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("aube")
.to_ascii_lowercase();
let subcommand = match stem.as_str() {
"aubr" => "run",
"aubx" => "dlx",
_ => return args,
};
args[0] = OsString::from("aube");
if matches!(
args.get(1).and_then(|s| s.to_str()),
Some("--version") | Some("-V")
) {
return args;
}
args.insert(1, OsString::from(subcommand));
args
}
fn normalize_npm_interpreter_shim_argv(args: &mut Vec<OsString>) {
let Some(shim) = args.get(1).cloned() else {
return;
};
let shim_path = std::path::Path::new(&shim);
let Some(stem) = shim_path.file_stem().and_then(|s| s.to_str()) else {
return;
};
if !matches!(stem, "aube" | "aubr" | "aubx") {
return;
}
let Ok(bytes) = std::fs::read(shim_path) else {
return;
};
if !bytes.starts_with(b"#!") {
return;
}
args[0] = shim;
args.remove(1);
}
fn lift_per_subcommand_flags(mut args: Vec<OsString>) -> Vec<OsString> {
const LIFTED_LONGS: &[(&str, bool)] = &[
("frozen-lockfile", false),
("no-frozen-lockfile", false),
("prefer-frozen-lockfile", false),
("registry", true),
("fetch-retries", true),
("fetch-retry-factor", true),
("fetch-retry-maxtimeout", true),
("fetch-retry-mintimeout", true),
("fetch-timeout", true),
("disable-global-virtual-store", false),
("disable-gvs", false),
("enable-global-virtual-store", false),
("enable-gvs", false),
];
const KEPT_LONGS_WITH_VALUE: &[&str] = &[
"dir",
"cd",
"prefix",
"loglevel",
"reporter",
"filter",
"filter-prod",
];
const KEPT_SHORTS_WITH_VALUE: &[&str] = &["-C", "-F"];
let token_looks_like_flag = |args: &[OsString], idx: usize| -> bool {
args.get(idx)
.and_then(|t| t.to_str())
.is_some_and(|s| s.starts_with('-') && s != "-")
};
let mut lifted: Vec<OsString> = Vec::new();
let mut subcommand_idx: Option<usize> = None;
let mut i = 1;
while i < args.len() {
let Some(s) = args[i].to_str() else { break };
if s == "--" {
break;
}
if let Some(rest) = s.strip_prefix("--") {
let (bare, has_inline_value) = match rest.split_once('=') {
Some((bare, _)) => (bare, true),
None => (rest, false),
};
if let Some((_, takes_value)) =
LIFTED_LONGS.iter().copied().find(|(name, _)| *name == bare)
{
lifted.push(args.remove(i));
if takes_value
&& !has_inline_value
&& i < args.len()
&& !token_looks_like_flag(&args, i)
{
lifted.push(args.remove(i));
}
continue;
}
if KEPT_LONGS_WITH_VALUE.contains(&bare) {
i += 1;
if !has_inline_value && i < args.len() && !token_looks_like_flag(&args, i) {
i += 1;
}
continue;
}
i += 1;
continue;
}
if s == "-" {
subcommand_idx = Some(i);
break;
}
if let Some(_rest) = s.strip_prefix('-') {
if s == "-F" {
i += 1;
if i < args.len() && !token_looks_like_flag(&args, i) {
i += 1;
}
continue;
}
if let Some(rest) = s.strip_prefix("-F")
&& !rest.is_empty()
{
i += 1;
continue;
}
if KEPT_SHORTS_WITH_VALUE.contains(&s) {
i += 1;
if i < args.len() && !token_looks_like_flag(&args, i) {
i += 1;
}
continue;
}
i += 1;
continue;
}
subcommand_idx = Some(i);
break;
}
if let Some(idx) = subcommand_idx {
let insert_at = idx + 1;
for (j, tok) in lifted.into_iter().enumerate() {
args.insert(insert_at + j, tok);
}
} else {
for tok in lifted.into_iter().rev() {
args.insert(1, tok);
}
}
args
}
#[derive(Parser)]
#[command(
name = "aube",
about = "A fast Node.js package manager",
version = version::VERSION_LONG.as_str(),
disable_version_flag = true
)]
pub(crate) struct Cli {
#[arg(short = 'C', long = "dir", visible_aliases = ["cd", "prefix"], global = true, value_name = "DIR")]
dir: Option<std::path::PathBuf>,
#[arg(short = 'F', long, global = true, value_name = "PATTERN")]
filter: Vec<String>,
#[arg(short = 'r', long, global = true)]
recursive: bool,
#[arg(short, long, global = true)]
verbose: bool,
#[arg(short = 'V', long = "version", global = true)]
version: bool,
#[arg(long, global = true, conflicts_with = "stream", hide = true)]
aggregate_output: bool,
#[arg(long, global = true, conflicts_with = "no_color")]
color: bool,
#[arg(long, global = true)]
fail_if_no_match: bool,
#[arg(long, global = true, value_name = "PATTERN")]
filter_prod: Vec<String>,
#[arg(long, global = true, hide = true)]
ignore_workspace: bool,
#[arg(long, global = true, hide = true)]
include_workspace_root: bool,
#[arg(long, global = true, value_name = "LEVEL", value_enum)]
loglevel: Option<LogLevel>,
#[arg(long, global = true)]
no_color: bool,
#[arg(long, global = true, value_name = "NAME", value_enum)]
reporter: Option<ReporterType>,
#[arg(long, global = true)]
silent: bool,
#[arg(long, global = true, conflicts_with = "aggregate_output", hide = true)]
stream: bool,
#[arg(long, global = true, hide = true)]
use_stderr: bool,
#[arg(long, global = true, hide = true)]
workspace_packages: bool,
#[arg(long, global = true)]
workspace_root: bool,
#[arg(short = 'y', long, global = true, hide = true)]
yes: bool,
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
#[clap(rename_all = "lowercase")]
pub(crate) enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
Silent,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
#[clap(rename_all = "kebab-case")]
pub(crate) enum ReporterType {
Default,
AppendOnly,
Ndjson,
Silent,
}
impl LogLevel {
fn filter(self) -> &'static str {
match self {
LogLevel::Trace => "trace",
LogLevel::Debug => "debug",
LogLevel::Info => "info",
LogLevel::Warn => "warn",
LogLevel::Error => "error",
LogLevel::Silent => "off",
}
}
}
struct SilentStderrGuard {
saved: libc::c_int,
}
impl SilentStderrGuard {
fn install() -> Option<Self> {
unsafe {
let saved = libc::dup(2);
if saved < 0 {
return None;
}
let devnull = libc::open(c"/dev/null".as_ptr(), libc::O_WRONLY);
if devnull < 0 {
libc::close(saved);
return None;
}
if libc::dup2(devnull, 2) < 0 {
libc::close(devnull);
libc::close(saved);
return None;
}
libc::close(devnull);
Some(Self { saved })
}
}
}
impl Drop for SilentStderrGuard {
fn drop(&mut self) {
unsafe {
libc::dup2(self.saved, 2);
libc::close(self.saved);
}
}
}
#[derive(Subcommand)]
enum Commands {
#[command(visible_alias = "a")]
Add(commands::add::AddArgs),
ApproveBuilds(commands::approve_builds::ApproveBuildsArgs),
#[command(after_long_help = commands::audit::AFTER_LONG_HELP)]
Audit(commands::audit::AuditArgs),
#[command(after_long_help = commands::bin::AFTER_LONG_HELP)]
Bin(commands::bin::BinArgs),
Cache(commands::cache::CacheArgs),
CatFile(commands::cat_file::CatFileArgs),
CatIndex(commands::cat_index::CatIndexArgs),
#[command(after_long_help = commands::check::AFTER_LONG_HELP)]
Check(commands::check::CheckArgs),
#[command(visible_alias = "clean-install", aliases = ["ic", "install-clean"])]
Ci(commands::ci::CiArgs),
Clean(commands::clean::CleanArgs),
Completion(commands::completion::CompletionArgs),
#[command(alias = "c")]
Config(commands::config::ConfigArgs),
Create(commands::create::CreateArgs),
Dedupe(commands::dedupe::DedupeArgs),
Deploy(commands::deploy::DeployArgs),
Deprecate(commands::deprecate::DeprecateArgs),
Deprecations(commands::deprecations::DeprecationsArgs),
#[command(visible_alias = "dist-tags")]
DistTag(commands::dist_tag::DistTagArgs),
Dlx(commands::dlx::DlxArgs),
#[command(after_long_help = commands::doctor::AFTER_LONG_HELP)]
Doctor(commands::doctor::DoctorArgs),
#[command(visible_alias = "x")]
Exec(commands::exec::ExecArgs),
Fetch(commands::fetch::FetchArgs),
#[command(after_long_help = commands::find_hash::AFTER_LONG_HELP)]
FindHash(commands::find_hash::FindHashArgs),
#[command(hide = true)]
Get(commands::config::GetArgs),
#[command(after_long_help = commands::ignored_builds::AFTER_LONG_HELP)]
IgnoredBuilds(commands::ignored_builds::IgnoredBuildsArgs),
Import(commands::import::ImportArgs),
Init(commands::init::InitArgs),
#[command(alias = "i")]
Install(commands::install::InstallArgs),
#[command(alias = "it", hide = true)]
InstallTest(commands::run::ScriptArgs),
#[command(hide = true)]
La(commands::list::ListArgs),
#[command(after_long_help = commands::licenses::AFTER_LONG_HELP)]
Licenses(commands::licenses::LicensesArgs),
#[command(visible_alias = "ln")]
Link(commands::link::LinkArgs),
#[command(visible_alias = "ls", after_long_help = commands::list::AFTER_LONG_HELP)]
List(commands::list::ListArgs),
#[command(hide = true)]
Ll(commands::list::ListArgs),
#[command(alias = "adduser")]
Login(commands::login::LoginArgs),
Logout(commands::logout::LogoutArgs),
#[command(after_long_help = commands::outdated::AFTER_LONG_HELP)]
Outdated(commands::outdated::OutdatedArgs),
#[command(hide = true)]
Owner(commands::npm_fallback::FallbackArgs),
Pack(commands::pack::PackArgs),
Patch(commands::patch::PatchArgs),
PatchCommit(commands::patch_commit::PatchCommitArgs),
PatchRemove(commands::patch_remove::PatchRemoveArgs),
Peers(commands::peers::PeersArgs),
#[command(hide = true)]
Pkg(commands::npm_fallback::FallbackArgs),
Prune(commands::prune::PruneArgs),
Publish(commands::publish::PublishArgs),
Purge(commands::clean::CleanArgs),
#[command(after_long_help = commands::query::AFTER_LONG_HELP)]
Query(commands::query::QueryArgs),
#[command(visible_alias = "rb")]
Rebuild(commands::rebuild::RebuildArgs),
#[command(visible_aliases = ["multi", "m"])]
Recursive(commands::recursive::RecursiveArgs),
#[command(visible_alias = "rm", aliases = ["uninstall", "un", "uni"])]
Remove(commands::remove::RemoveArgs),
Restart(commands::run::ScriptArgs),
#[command(after_long_help = commands::root::AFTER_LONG_HELP)]
Root(commands::root::RootArgs),
#[command(alias = "run-script")]
Run(commands::run::RunArgs),
Sbom(commands::sbom::SbomArgs),
#[command(hide = true)]
Search(commands::npm_fallback::FallbackArgs),
#[command(hide = true)]
Set(commands::config::SetArgs),
#[command(hide = true, name = "set-script")]
SetScript(commands::npm_fallback::FallbackArgs),
Start(commands::run::ScriptArgs),
Stop(commands::run::ScriptArgs),
Store(commands::store::StoreArgs),
#[command(visible_alias = "t")]
Test(commands::run::ScriptArgs),
#[command(hide = true)]
Token(commands::npm_fallback::FallbackArgs),
Undeprecate(commands::undeprecate::UndeprecateArgs),
#[command(alias = "dislink")]
Unlink(commands::unlink::UnlinkArgs),
Unpublish(commands::unpublish::UnpublishArgs),
#[command(aliases = ["up", "upgrade"])]
Update(commands::update::UpdateArgs),
#[command(hide = true)]
Usage,
Version(commands::version::VersionArgs),
#[command(visible_aliases = ["info", "show"], alias = "v", after_long_help = commands::view::AFTER_LONG_HELP)]
View(commands::view::ViewArgs),
#[command(hide = true)]
Whoami(commands::npm_fallback::FallbackArgs),
#[command(visible_alias = "w", after_long_help = commands::why::AFTER_LONG_HELP)]
Why(commands::why::WhyArgs),
#[command(external_subcommand)]
External(Vec<String>),
}
fn main() {
if let Err(report) = inner_main() {
eprintln!("{report:?}");
std::process::exit(report_exit_code(&report));
}
}
fn report_exit_code(report: &miette::Report) -> i32 {
if let Some(code) = report.code() {
let code = code.to_string();
if let Some(exit) = aube_codes::exit::exit_code_for(&code) {
return exit;
}
}
aube_codes::exit::EXIT_GENERIC
}
fn inner_main() -> miette::Result<()> {
let mut argv: Vec<OsString> = std::env::args_os().collect();
let config_overrides = extract_config_overrides(&mut argv);
aube_settings::set_global_cli_overrides(config_overrides);
let cli = Cli::parse_from(lift_per_subcommand_flags(rewrite_multicall_argv(argv)));
let color_mode = resolve_color_mode(&cli);
if matches!(color_mode, ColorMode::Never) {
unsafe {
std::env::set_var("NO_COLOR", "1");
std::env::remove_var("FORCE_COLOR");
std::env::remove_var("CLICOLOR_FORCE");
}
} else if matches!(color_mode, ColorMode::Always) {
unsafe {
std::env::set_var("FORCE_COLOR", "1");
std::env::set_var("CLICOLOR_FORCE", "1");
std::env::remove_var("NO_COLOR");
}
} else if ci_renders_ansi() && !env_disables_color() {
console::set_colors_enabled_stderr(true);
}
let is_silent = cli.silent || matches!(cli.reporter, Some(ReporterType::Silent));
if !is_silent {
let use_stderr_active = cli.use_stderr
|| startup_cwd(&cli).ok().is_some_and(|cwd| {
let npmrc = aube_registry::config::load_npmrc_entries(&cwd);
let aube_config = commands::config::load_user_aube_config_entries();
let ws = std::collections::BTreeMap::new();
let env_snap = aube_settings::values::capture_env();
let ctx = aube_settings::ResolveCtx {
npmrc: &npmrc,
aube_config: &aube_config,
workspace_yaml: &ws,
env: &env_snap,
cli: &[],
};
aube_settings::resolved::use_stderr(&ctx)
});
if use_stderr_active {
unsafe {
libc::dup2(2, 1);
}
}
}
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.into_diagnostic()
.wrap_err("failed to build tokio runtime")?;
let exit_code = runtime.block_on(async_main(cli))?;
drop(runtime);
if let Some(exit_code) = exit_code {
std::process::exit(exit_code);
}
Ok(())
}
async fn async_main(cli: Cli) -> miette::Result<Option<i32>> {
if let Some(dir) = &cli.dir {
std::env::set_current_dir(dir)
.into_diagnostic()
.wrap_err_with(|| format!("failed to change directory to {}", dir.display()))?;
}
if cli.version {
println!("{}", crate::version::VERSION_LONG.as_str());
let cwd =
crate::dirs::project_root_or_cwd().unwrap_or_else(|_| std::path::PathBuf::from("."));
update_check::check_and_notify(&cwd).await;
return Ok(None);
}
if cli.workspace_root {
let start = std::env::current_dir()
.into_diagnostic()
.wrap_err("failed to read current dir")?;
let root = commands::find_workspace_root(&start)?;
if root != start {
std::env::set_current_dir(&root)
.into_diagnostic()
.wrap_err_with(|| format!("failed to change directory to {}", root.display()))?;
}
crate::dirs::set_cwd(&root)?;
}
let settings = load_startup_settings()?;
let effective_level = resolve_loglevel(&cli, settings.loglevel.as_deref());
init_logging(&cli, effective_level);
raise_nofile_limit();
let _silent_guard = matches!(effective_level, LogLevel::Silent)
.then(SilentStderrGuard::install)
.flatten();
if let Some(ref guard) = _silent_guard {
aube_scripts::set_saved_stderr_fd(guard.saved);
}
commands::set_skip_auto_install_on_package_manager_mismatch(false);
if command_needs_package_manager_guard(cli.command.as_ref()) {
let guard = enforce_package_manager_guardrails(&settings, cli.command.as_ref())?;
commands::set_skip_auto_install_on_package_manager_mismatch(
guard == PackageManagerGuard::WarnRunOnly,
);
}
let effective_filter = compute_effective_filter(&cli);
commands::set_global_output_flags(commands::GlobalOutputFlags {
silent: matches!(effective_level, LogLevel::Silent),
});
match cli.command {
Some(Commands::Add(args)) => {
commands::add::run(args, effective_filter.clone()).await?;
}
Some(Commands::ApproveBuilds(args)) => commands::approve_builds::run(args).await?,
Some(Commands::Audit(args)) => commands::audit::run(args).await?,
Some(Commands::Bin(args)) => commands::bin::run(args).await?,
Some(Commands::Cache(args)) => commands::cache::run(args).await?,
Some(Commands::CatFile(args)) => commands::cat_file::run(args).await?,
Some(Commands::CatIndex(args)) => commands::cat_index::run(args).await?,
Some(Commands::Check(args)) => commands::check::run(args).await?,
Some(Commands::Ci(args)) => commands::ci::run(args).await?,
Some(Commands::Clean(args)) => commands::clean::run(args).await?,
Some(Commands::Completion(args)) => commands::completion::run(args).await?,
Some(Commands::Config(args)) => commands::config::run(args).await?,
Some(Commands::Create(args)) => commands::create::run(args).await?,
Some(Commands::Dedupe(args)) => commands::dedupe::run(args).await?,
Some(Commands::Deploy(args)) => {
commands::deploy::run(args, effective_filter.clone()).await?
}
Some(Commands::Deprecate(args)) => commands::deprecate::run(args).await?,
Some(Commands::Deprecations(args)) => {
if let Some(code) = commands::deprecations::run(args).await? {
return Ok(Some(code));
}
}
Some(Commands::DistTag(args)) => commands::dist_tag::run(args).await?,
Some(Commands::Dlx(args)) => commands::dlx::run(args).await?,
Some(Commands::Doctor(args)) => commands::doctor::run(args).await?,
Some(Commands::Exec(args)) => commands::exec::run(args, effective_filter.clone()).await?,
Some(Commands::Fetch(args)) => commands::fetch::run(args).await?,
Some(Commands::FindHash(args)) => commands::find_hash::run(args).await?,
Some(Commands::Get(args)) => commands::config::get(args)?,
Some(Commands::IgnoredBuilds(args)) => commands::ignored_builds::run(args).await?,
Some(Commands::Import(args)) => commands::import::run(args).await?,
Some(Commands::Init(args)) => commands::init::run(args).await?,
Some(Commands::Install(args)) => {
run_install_command(args, effective_filter.clone(), cli.workspace_root).await?;
}
Some(Commands::InstallTest(args)) => commands::install_test::run(args).await?,
Some(Commands::La(mut args)) | Some(Commands::Ll(mut args)) => {
args.long = true;
commands::list::run(args, effective_filter.clone()).await?;
}
Some(Commands::Licenses(args)) => commands::licenses::run(args).await?,
Some(Commands::Link(args)) => commands::link::run(args).await?,
Some(Commands::List(args)) => commands::list::run(args, effective_filter.clone()).await?,
Some(Commands::Login(args)) => commands::login::run(args).await?,
Some(Commands::Logout(args)) => commands::logout::run(args).await?,
Some(Commands::Outdated(args)) => {
commands::outdated::run(args, effective_filter.clone()).await?
}
Some(Commands::Owner(args)) => {
return Ok(Some(commands::npm_fallback::run("owner", &args)?));
}
Some(Commands::Pack(args)) => commands::pack::run(args).await?,
Some(Commands::Patch(args)) => commands::patch::run(args).await?,
Some(Commands::PatchCommit(args)) => commands::patch_commit::run(args).await?,
Some(Commands::PatchRemove(args)) => commands::patch_remove::run(args).await?,
Some(Commands::Peers(args)) => commands::peers::run(args).await?,
Some(Commands::Pkg(args)) => {
return Ok(Some(commands::npm_fallback::run("pkg", &args)?));
}
Some(Commands::Prune(args)) => commands::prune::run(args).await?,
Some(Commands::Publish(args)) => {
commands::publish::run(args, effective_filter.clone()).await?
}
Some(Commands::Purge(args)) => commands::clean::run_purge(args).await?,
Some(Commands::Query(args)) => commands::query::run(args, effective_filter.clone()).await?,
Some(Commands::Rebuild(args)) => {
commands::rebuild::run(args, effective_filter.clone()).await?
}
Some(Commands::Remove(args)) => {
commands::remove::run(args, effective_filter.clone()).await?
}
Some(Commands::Recursive(args)) => {
let argv = commands::recursive::argv(
args,
commands::recursive::RecursiveGlobals {
filters: effective_filter.clone(),
color: cli.color,
no_color: cli.no_color,
},
)?;
let nested_argv: Vec<OsString> =
lift_per_subcommand_flags(argv.into_iter().map(OsString::from).collect());
let nested = Cli::try_parse_from(nested_argv).into_diagnostic()?;
let nested_filter = compute_effective_filter(&nested);
match nested.command {
Some(Commands::Add(args)) => {
commands::add::run(args, nested_filter).await?;
}
Some(Commands::Deploy(args)) => commands::deploy::run(args, nested_filter).await?,
Some(Commands::Exec(args)) => commands::exec::run(args, nested_filter).await?,
Some(Commands::Install(args)) => {
run_install_command(args, nested_filter, nested.workspace_root).await?;
}
Some(Commands::List(args)) => commands::list::run(args, nested_filter).await?,
Some(Commands::La(mut args)) | Some(Commands::Ll(mut args)) => {
args.long = true;
commands::list::run(args, nested_filter).await?;
}
Some(Commands::Outdated(args)) => {
commands::outdated::run(args, nested_filter).await?
}
Some(Commands::Publish(args)) => {
commands::publish::run(args, nested_filter).await?
}
Some(Commands::Rebuild(args)) => {
commands::rebuild::run(args, nested_filter).await?
}
Some(Commands::Remove(args)) => commands::remove::run(args, nested_filter).await?,
Some(Commands::Restart(args)) => {
commands::restart::run(args, nested_filter).await?
}
Some(Commands::Run(args)) => commands::run::run(args, nested_filter).await?,
Some(Commands::Start(args)) => {
run_script_lifecycle("start", args, &nested_filter).await?;
}
Some(Commands::Stop(args)) => {
run_script_lifecycle("stop", args, &nested_filter).await?;
}
Some(Commands::Test(args)) => {
run_script_lifecycle("test", args, &nested_filter).await?;
}
Some(Commands::Update(args)) => {
commands::update::run(args, nested_filter).await?;
}
Some(Commands::Why(args)) => commands::why::run(args, nested_filter).await?,
Some(Commands::External(args)) => {
let script = &args[0];
let script_args: Vec<String> = args[1..].to_vec();
commands::run::run_script(script, &script_args, false, false, &nested_filter)
.await?;
}
Some(_) | None => {
return Err(miette::miette!(
code = aube_codes::errors::ERR_AUBE_RECURSIVE_NOT_SUPPORTED,
"aube recursive: command does not support recursive execution"
));
}
}
}
Some(Commands::Restart(args)) => {
commands::restart::run(args, effective_filter.clone()).await?
}
Some(Commands::Root(args)) => commands::root::run(args).await?,
Some(Commands::Run(args)) => commands::run::run(args, effective_filter.clone()).await?,
Some(Commands::Sbom(args)) => commands::sbom::run(args).await?,
Some(Commands::Search(args)) => {
return Ok(Some(commands::npm_fallback::run("search", &args)?));
}
Some(Commands::Set(args)) => commands::config::set(args)?,
Some(Commands::SetScript(args)) => {
return Ok(Some(commands::npm_fallback::run("set-script", &args)?));
}
Some(Commands::Start(args)) => {
run_script_lifecycle("start", args, &effective_filter).await?;
}
Some(Commands::Stop(args)) => {
run_script_lifecycle("stop", args, &effective_filter).await?;
}
Some(Commands::Store(args)) => commands::store::run(args).await?,
Some(Commands::Test(args)) => {
run_script_lifecycle("test", args, &effective_filter).await?;
}
Some(Commands::Token(args)) => {
return Ok(Some(commands::npm_fallback::run("token", &args)?));
}
Some(Commands::Undeprecate(args)) => commands::undeprecate::run(args).await?,
Some(Commands::Unlink(args)) => commands::unlink::run(args).await?,
Some(Commands::Unpublish(args)) => commands::unpublish::run(args).await?,
Some(Commands::Update(args)) => {
commands::update::run(args, effective_filter.clone()).await?;
}
Some(Commands::Version(args)) => commands::version::run(args).await?,
Some(Commands::View(args)) => commands::view::run(args).await?,
Some(Commands::Whoami(args)) => {
return Ok(Some(commands::npm_fallback::run("whoami", &args)?));
}
Some(Commands::Why(args)) => commands::why::run(args, effective_filter.clone()).await?,
Some(Commands::Usage) => {
use clap::CommandFactory;
let mut cmd = Cli::command().version(env!("CARGO_PKG_VERSION"));
clap_usage::generate(&mut cmd, "aube", &mut std::io::stdout());
}
Some(Commands::External(args)) => {
let script = &args[0];
let script_args: Vec<String> = args[1..].to_vec();
if effective_filter.is_empty() {
let initial_cwd = crate::dirs::cwd()?;
let script_exists = crate::dirs::find_project_root(&initial_cwd)
.and_then(|cwd| {
aube_manifest::PackageJson::from_path(&cwd.join("package.json")).ok()
})
.map(|m| m.scripts.contains_key(script))
.unwrap_or(false);
if !script_exists {
use clap::CommandFactory;
let mut cmd = Cli::command();
cmd.print_help().ok();
eprintln!();
return Err(miette::miette!(
code = aube_codes::errors::ERR_AUBE_UNKNOWN_COMMAND,
"unknown command: {script}"
));
}
}
commands::run::run_script(script, &script_args, false, false, &effective_filter)
.await?;
}
None => {
use clap::CommandFactory;
let mut cmd = Cli::command();
cmd.print_help().ok();
println!();
}
}
Ok(None)
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum ColorMode {
Auto,
Always,
Never,
}
#[derive(Debug)]
struct StartupSettings {
loglevel: Option<String>,
package_manager_strict: PackageManagerStrictMode,
package_manager_strict_version: bool,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
enum PackageManagerStrictMode {
Off,
#[default]
Warn,
Error,
}
impl PackageManagerStrictMode {
fn parse(raw: &str) -> Option<Self> {
match raw.trim().to_ascii_lowercase().as_str() {
"off" | "false" | "0" => Some(Self::Off),
"warn" => Some(Self::Warn),
"error" | "true" | "1" => Some(Self::Error),
_ => None,
}
}
}
fn resolve_package_manager_strict(ctx: &aube_settings::ResolveCtx<'_>) -> PackageManagerStrictMode {
let raw = aube_settings::resolved::package_manager_strict(ctx);
if let Some(mode) = PackageManagerStrictMode::parse(&raw) {
return mode;
}
eprintln!(
"warning: packageManagerStrict={raw:?} is not a recognized value (expected `off`, `warn`, `error`, or back-compat bool `true`/`false`); falling back to `warn`."
);
PackageManagerStrictMode::default()
}
fn resolve_color_mode(cli: &Cli) -> ColorMode {
if cli.no_color {
return ColorMode::Never;
}
if cli.color {
return ColorMode::Always;
}
let env = aube_settings::values::capture_env();
if let Some(mode) =
aube_settings::values::string_from_env("color", &env).and_then(|raw| parse_color_mode(&raw))
{
return mode;
}
let Ok(cwd) = startup_cwd(cli) else {
return ColorMode::Auto;
};
let npmrc = aube_registry::config::load_npmrc_entries(&cwd);
aube_settings::values::string_from_npmrc("color", &npmrc)
.and_then(|raw| parse_color_mode(&raw))
.unwrap_or(ColorMode::Auto)
}
fn ci_renders_ansi() -> bool {
use ci_info::types::Vendor;
matches!(
ci_info::get().vendor,
Some(
Vendor::GitHubActions
| Vendor::GitLabCI
| Vendor::Buildkite
| Vendor::CircleCI
| Vendor::TravisCI
| Vendor::Drone
| Vendor::AppVeyor
| Vendor::AzurePipelines
| Vendor::BitbucketPipelines
| Vendor::TeamCity
| Vendor::WoodpeckerCI
)
)
}
fn env_disables_color() -> bool {
std::env::var_os("NO_COLOR").is_some_and(|v| !v.is_empty())
|| std::env::var_os("CLICOLOR").is_some_and(|v| v == "0")
}
fn startup_cwd(cli: &Cli) -> miette::Result<PathBuf> {
let cwd = match &cli.dir {
Some(dir) if dir.is_absolute() => Ok(dir.clone()),
Some(dir) => std::env::current_dir()
.into_diagnostic()
.map(|cwd| cwd.join(dir)),
None => std::env::current_dir().into_diagnostic(),
}?;
if cli.workspace_root {
commands::find_workspace_root(&cwd)
} else {
Ok(cwd)
}
}
fn load_startup_settings() -> miette::Result<StartupSettings> {
let cwd = std::env::current_dir().into_diagnostic()?;
let npmrc = aube_registry::config::load_npmrc_entries(&cwd);
let aube_config = commands::config::load_user_aube_config_entries();
let empty_ws = std::collections::BTreeMap::new();
let env = aube_settings::values::capture_env();
let ctx = aube_settings::ResolveCtx {
npmrc: &npmrc,
aube_config: &aube_config,
workspace_yaml: &empty_ws,
env: &env,
cli: &[],
};
Ok(StartupSettings {
loglevel: aube_settings::values::string_from_env("loglevel", &env)
.or_else(|| aube_settings::values::string_from_npmrc("loglevel", &npmrc)),
package_manager_strict: resolve_package_manager_strict(&ctx),
package_manager_strict_version: aube_settings::resolved::package_manager_strict_version(
&ctx,
),
})
}
fn resolve_loglevel(cli: &Cli, configured: Option<&str>) -> LogLevel {
let reporter_silent = matches!(cli.reporter, Some(ReporterType::Silent));
if cli.silent || reporter_silent {
return LogLevel::Silent;
}
if let Some(level) = cli.loglevel {
return level;
}
if env_is_truthy("AUBE_TRACE") {
return LogLevel::Trace;
}
if cli.verbose || env_is_truthy("AUBE_DEBUG") {
return LogLevel::Debug;
}
configured
.and_then(parse_loglevel)
.unwrap_or(LogLevel::Warn)
}
fn env_is_truthy(name: &str) -> bool {
let Ok(raw) = std::env::var(name) else {
return false;
};
matches!(
raw.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "y"
)
}
fn parse_loglevel(raw: &str) -> Option<LogLevel> {
match raw.trim().to_ascii_lowercase().as_str() {
"trace" => Some(LogLevel::Trace),
"debug" => Some(LogLevel::Debug),
"info" => Some(LogLevel::Info),
"warn" | "warning" => Some(LogLevel::Warn),
"error" => Some(LogLevel::Error),
"silent" => Some(LogLevel::Silent),
_ => None,
}
}
fn parse_color_mode(raw: &str) -> Option<ColorMode> {
match raw.trim().to_ascii_lowercase().as_str() {
"always" | "true" | "1" => Some(ColorMode::Always),
"never" | "false" | "0" => Some(ColorMode::Never),
"auto" => Some(ColorMode::Auto),
_ => None,
}
}
#[cfg(unix)]
fn raise_nofile_limit() {
unsafe {
let mut rlim = std::mem::zeroed::<libc::rlimit>();
if libc::getrlimit(libc::RLIMIT_NOFILE, &mut rlim) != 0 {
tracing::trace!("getrlimit(RLIMIT_NOFILE) failed; keeping default FD limit");
return;
}
let before = rlim.rlim_cur;
if before >= rlim.rlim_max {
tracing::trace!("RLIMIT_NOFILE soft={before} already at hard limit");
return;
}
let hard = rlim.rlim_max;
rlim.rlim_cur = hard;
if libc::setrlimit(libc::RLIMIT_NOFILE, &rlim) == 0 {
tracing::trace!("raised RLIMIT_NOFILE soft {before} -> {hard}");
return;
}
rlim.rlim_cur = before.max(10240).min(hard);
if libc::setrlimit(libc::RLIMIT_NOFILE, &rlim) == 0 {
tracing::trace!(
"raised RLIMIT_NOFILE soft {before} -> {} (hard={hard}, fallback cap)",
rlim.rlim_cur
);
} else {
tracing::trace!("setrlimit(RLIMIT_NOFILE) failed; keeping soft={before}");
}
}
}
#[cfg(not(unix))]
fn raise_nofile_limit() {}
fn init_logging(cli: &Cli, effective_level: LogLevel) {
let log_level = effective_level.filter();
let env_filter = tracing_subscriber::EnvFilter::try_from_env("AUBE_LOG").unwrap_or_else(|_| {
format!(
"aube={log_level},aube_cli={log_level},aube_registry={log_level},\
aube_resolver={log_level},aube_lockfile={log_level},aube_store={log_level},\
aube_linker={log_level},aube_manifest={log_level},aube_scripts={log_level},\
aube_workspace={log_level},aube_settings={log_level},aube_util={log_level}"
)
.into()
});
let drop_timestamp = !matches!(effective_level, LogLevel::Debug | LogLevel::Trace);
let registry = tracing_subscriber::registry().with(env_filter);
if matches!(cli.reporter, Some(ReporterType::Ndjson)) {
crate::pnpmfile::set_ndjson_reporter(true);
registry
.with(
tracing_subscriber::fmt::layer()
.json()
.flatten_event(true)
.with_writer(crate::progress::PausingWriter),
)
.init();
} else if drop_timestamp {
registry
.with(
tracing_subscriber::fmt::layer()
.without_time()
.with_writer(crate::progress::PausingWriter),
)
.init();
} else {
registry
.with(tracing_subscriber::fmt::layer().with_writer(crate::progress::PausingWriter))
.init();
}
let force_text = matches!(
effective_level,
LogLevel::Trace | LogLevel::Debug | LogLevel::Silent
) || matches!(
cli.reporter,
Some(ReporterType::AppendOnly) | Some(ReporterType::Ndjson)
);
if force_text {
clx::progress::set_output(clx::progress::ProgressOutput::Text);
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum PackageManagerGuard {
Ok,
WarnRunOnly,
}
fn enforce_package_manager_guardrails(
settings: &StartupSettings,
command: Option<&Commands>,
) -> miette::Result<PackageManagerGuard> {
if settings.package_manager_strict == PackageManagerStrictMode::Off {
return Ok(PackageManagerGuard::Ok);
}
let cwd = std::env::current_dir().into_diagnostic()?;
let Some(root) = crate::dirs::find_workspace_root(&cwd)
.filter(|root| root.join("package.json").is_file())
.or_else(|| crate::dirs::find_project_root(&cwd))
else {
return Ok(PackageManagerGuard::Ok);
};
let path = root.join("package.json");
let raw = std::fs::read_to_string(&path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to read {}", path.display()))?;
let json: serde_json::Value = serde_json::from_str(&raw)
.into_diagnostic()
.wrap_err_with(|| format!("failed to parse {}", path.display()))?;
let Some(package_manager) = json.get("packageManager").and_then(|v| v.as_str()) else {
return Ok(PackageManagerGuard::Ok);
};
let Some((name, version)) = parse_package_manager(package_manager) else {
return Err(miette!(
"invalid packageManager field `{package_manager}` in {}",
path.display()
));
};
let normalized = version.strip_suffix("-DEBUG").unwrap_or(version);
match name {
"aube" => {
if settings.package_manager_strict_version && normalized != env!("CARGO_PKG_VERSION") {
return Err(miette!(
"packageManager requires aube@{version}, but this is aube@{}",
env!("CARGO_PKG_VERSION")
));
}
Ok(PackageManagerGuard::Ok)
}
"pnpm" => {
if settings.package_manager_strict_version {
return Err(miette!(
"packageManager requires exact pnpm@{version}, but aube cannot download or re-exec a specific pnpm version. Use pnpm directly, set packageManagerStrictVersion=false, or pin packageManager to aube@{}.",
env!("CARGO_PKG_VERSION")
));
}
Ok(PackageManagerGuard::Ok)
}
other => {
let mode = match settings.package_manager_strict {
PackageManagerStrictMode::Error => package_manager_guard_mode(command),
_ => PackageManagerGuardMode::WarnAndSkipAutoInstall,
};
match mode {
PackageManagerGuardMode::Error => Err(miette!(
"packageManager in {} uses unsupported package manager `{other}`. aube's packageManagerStrict=error guard only accepts `aube` and `pnpm`; remove or change the `packageManager` field, or set `package-manager-strict=warn` (the default) or `=off` in .npmrc to soften this guard.",
path.display()
)),
PackageManagerGuardMode::WarnAndSkipAutoInstall => {
eprintln!(
"warning: packageManager in {} uses unsupported package manager `{other}`; continuing but auto-install is disabled. Switch packageManager to `aube`/`pnpm`, set packageManagerStrict=off, or pass `--no-install` to skip the install probe explicitly.",
path.display()
);
Ok(PackageManagerGuard::WarnRunOnly)
}
}
}
}
}
fn parse_package_manager(raw: &str) -> Option<(&str, &str)> {
let (name, rest) = raw.rsplit_once('@')?;
if name.is_empty() || rest.is_empty() {
return None;
}
let version = rest.split_once('+').map_or(rest, |(version, _)| version);
if version.is_empty() {
return None;
}
Some((name, version))
}
fn command_needs_package_manager_guard(command: Option<&Commands>) -> bool {
!matches!(
command,
None | Some(Commands::Config(_))
| Some(Commands::Get(_))
| Some(Commands::Set(_))
| Some(Commands::Completion(_))
| Some(Commands::Usage)
)
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum PackageManagerGuardMode {
Error,
WarnAndSkipAutoInstall,
}
fn package_manager_guard_mode(command: Option<&Commands>) -> PackageManagerGuardMode {
if matches!(
command,
Some(Commands::Run(_))
| Some(Commands::Test(_))
| Some(Commands::Start(_))
| Some(Commands::Stop(_))
| Some(Commands::Restart(_))
| Some(Commands::External(_))
) {
PackageManagerGuardMode::WarnAndSkipAutoInstall
} else {
PackageManagerGuardMode::Error
}
}
fn compute_effective_filter(cli: &Cli) -> aube_workspace::selector::EffectiveFilter {
let mut filters = cli.filter.clone();
if cli.recursive && filters.is_empty() && cli.filter_prod.is_empty() {
filters.push("*".to_string());
}
aube_workspace::selector::EffectiveFilter {
filters,
filter_prods: cli.filter_prod.clone(),
fail_if_no_match: cli.fail_if_no_match,
}
}
async fn run_script_lifecycle(
name: &str,
args: commands::run::ScriptArgs,
filter: &aube_workspace::selector::EffectiveFilter,
) -> miette::Result<()> {
args.network.install_overrides();
args.lockfile.install_overrides();
args.virtual_store.install_overrides();
commands::run::run_script(name, &args.args, args.no_install, false, filter).await
}
async fn run_install_command(
args: commands::install::InstallArgs,
filter: aube_workspace::selector::EffectiveFilter,
workspace_root_already: bool,
) -> miette::Result<()> {
if args.workspace_root_short && !workspace_root_already {
let start = std::env::current_dir()
.into_diagnostic()
.wrap_err("failed to read current dir")?;
let root = commands::find_workspace_root(&start)?;
if root != start {
std::env::set_current_dir(&root)
.into_diagnostic()
.wrap_err_with(|| format!("failed to change directory to {}", root.display()))?;
}
crate::dirs::set_cwd(&root)?;
}
args.network.install_overrides();
args.lockfile.install_overrides();
args.virtual_store.install_overrides();
let global_frozen = args.lockfile.frozen_override();
let global_gvs = args.virtual_store.flags();
let cwd = crate::dirs::workspace_or_project_root()?;
let npmrc = aube_registry::config::load_npmrc_entries(&cwd);
let raw_ws = aube_manifest::workspace::load_raw(&cwd)
.into_diagnostic()
.wrap_err("failed to load workspace config")?;
let env = aube_settings::values::capture_env();
let cli_flags = args.to_cli_flag_bag(global_frozen, global_gvs);
let aube_config = commands::config::load_user_aube_config_entries();
let ctx = aube_settings::ResolveCtx {
npmrc: &npmrc,
aube_config: &aube_config,
workspace_yaml: &raw_ws,
env: &env,
cli: &cli_flags,
};
let yaml_prefer_frozen = aube_settings::resolved::prefer_frozen_lockfile(&ctx);
let mut opts = args.into_options(global_frozen, yaml_prefer_frozen, cli_flags, env);
opts.workspace_filter = filter;
commands::install::run(opts).await?;
Ok(())
}
#[cfg(test)]
mod cli_spec_tests {
use super::*;
#[test]
fn install_accepts_subcommand_registry_flag() {
let cli = Cli::try_parse_from([
"aube",
"install",
"--registry",
"https://registry.example.com/",
])
.expect("install --registry should parse");
let Some(Commands::Install(install_args)) = cli.command else {
panic!("expected install subcommand");
};
assert_eq!(
install_args.network.registry.as_deref(),
Some("https://registry.example.com/")
);
}
#[test]
fn pre_subcommand_registry_lifts_to_install() {
let argv = lift_per_subcommand_flags(
[
"aube",
"--registry",
"https://registry.example.com/",
"install",
]
.into_iter()
.map(OsString::from)
.collect(),
);
let cli = Cli::try_parse_from(argv)
.expect("pre-subcommand --registry should still parse via the rewriter");
let Some(Commands::Install(install_args)) = cli.command else {
panic!("expected install subcommand");
};
assert_eq!(
install_args.network.registry.as_deref(),
Some("https://registry.example.com/")
);
}
#[test]
fn lifter_does_not_eat_lifted_flag_as_kept_flag_value() {
let argv = lift_per_subcommand_flags(
["aube", "--dir", "--frozen-lockfile", "install"]
.into_iter()
.map(OsString::from)
.collect(),
);
let strs: Vec<&str> = argv.iter().filter_map(|t| t.to_str()).collect();
let install_idx = strs
.iter()
.position(|s| *s == "install")
.expect("install subcommand should survive the lift");
assert!(
strs[install_idx + 1..].contains(&"--frozen-lockfile"),
"--frozen-lockfile should land after the subcommand: {strs:?}"
);
}
#[test]
fn short_command_aliases_parse() {
let cli = Cli::try_parse_from(["aube", "a", "react"]).expect("a should parse as add");
assert!(matches!(cli.command, Some(Commands::Add(_))));
let cli =
Cli::try_parse_from(["aube", "x", "vitest", "--run"]).expect("x should parse as exec");
let Some(Commands::Exec(args)) = cli.command else {
panic!("x should dispatch to exec");
};
assert_eq!(args.bin, "vitest");
assert_eq!(args.args, vec!["--run"]);
let cli = Cli::try_parse_from(["aube", "w", "react"]).expect("w should parse as why");
assert!(matches!(cli.command, Some(Commands::Why(_))));
}
}
#[cfg(test)]
mod multicall_tests {
use super::*;
fn os(strs: &[&str]) -> Vec<OsString> {
strs.iter().map(OsString::from).collect()
}
fn temp_shim(name: &str) -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("temp dir should be created");
std::fs::write(dir.path().join(name), "#!/tmp/aube.exe\n").expect("shim should be written");
dir
}
#[test]
fn aube_passes_through_unchanged() {
assert_eq!(
rewrite_multicall_argv(os(&["aube", "install"])),
os(&["aube", "install"])
);
}
#[test]
fn aubr_rewrites_to_run() {
assert_eq!(
rewrite_multicall_argv(os(&["aubr", "build"])),
os(&["aube", "run", "build"])
);
}
#[test]
fn aubx_rewrites_to_dlx() {
assert_eq!(
rewrite_multicall_argv(os(&["aubx", "cowsay", "hi"])),
os(&["aube", "dlx", "cowsay", "hi"])
);
}
#[test]
fn absolute_path_and_exe_suffix_are_handled() {
assert_eq!(
rewrite_multicall_argv(os(&["/usr/local/bin/aubr", "test"])),
os(&["aube", "run", "test"])
);
assert_eq!(
rewrite_multicall_argv(os(&["aubx.exe", "pkg"])),
os(&["aube", "dlx", "pkg"])
);
}
#[test]
fn bare_shim_invocation_passes_through_to_subcommand() {
assert_eq!(rewrite_multicall_argv(os(&["aubr"])), os(&["aube", "run"]));
}
#[test]
fn version_flag_short_circuits_to_top_level() {
assert_eq!(
rewrite_multicall_argv(os(&["aubr", "--version"])),
os(&["aube", "--version"])
);
assert_eq!(
rewrite_multicall_argv(os(&["aubx", "--version"])),
os(&["aube", "--version"])
);
assert_eq!(
rewrite_multicall_argv(os(&["aubr", "-V"])),
os(&["aube", "-V"])
);
assert_eq!(
rewrite_multicall_argv(os(&["aubx.exe", "-V"])),
os(&["aube", "-V"])
);
}
#[test]
fn npm_interpreter_shim_path_is_dropped() {
let dir = temp_shim("aube");
let shim = dir.path().join("aube");
let shim_os = shim.clone().into_os_string();
assert_eq!(
rewrite_multicall_argv(vec![
OsString::from("aube.exe"),
shim.into_os_string(),
OsString::from("--version"),
]),
vec![shim_os, OsString::from("--version")]
);
}
#[test]
fn npm_interpreter_shim_preserves_multicall_dispatch() {
let dir = temp_shim("aubr");
let shim = dir.path().join("aubr");
assert_eq!(
rewrite_multicall_argv(vec![
OsString::from("aubr.exe"),
shim.into_os_string(),
OsString::from("build"),
]),
os(&["aube", "run", "build"])
);
}
#[test]
fn extract_config_overrides_strips_equals_form() {
let mut argv = os(&["aube", "install", "--config.strict-dep-builds=true"]);
let parsed = extract_config_overrides(&mut argv);
assert_eq!(argv, os(&["aube", "install"]));
assert_eq!(
parsed,
vec![("strict-dep-builds".to_string(), "true".to_string())]
);
}
#[test]
fn extract_config_overrides_strips_bool_form() {
let mut argv = os(&["aube", "--config.strictDepBuilds", "install"]);
let parsed = extract_config_overrides(&mut argv);
assert_eq!(argv, os(&["aube", "install"]));
assert_eq!(
parsed,
vec![("strictDepBuilds".to_string(), "true".to_string())]
);
}
#[test]
fn extract_config_overrides_handles_multiple_and_preserves_order() {
let mut argv = os(&[
"aube",
"--config.foo=1",
"install",
"--config.bar=two",
"--config.foo=3",
]);
let parsed = extract_config_overrides(&mut argv);
assert_eq!(argv, os(&["aube", "install"]));
assert_eq!(
parsed,
vec![
("foo".to_string(), "1".to_string()),
("bar".to_string(), "two".to_string()),
("foo".to_string(), "3".to_string()),
]
);
}
#[test]
fn extract_config_overrides_stops_at_double_dash() {
let mut argv = os(&["aube", "exec", "--", "node", "--config.foo=should-stay"]);
let parsed = extract_config_overrides(&mut argv);
assert!(parsed.is_empty());
assert_eq!(
argv,
os(&["aube", "exec", "--", "node", "--config.foo=should-stay"])
);
}
#[test]
fn extract_config_overrides_preserves_argv_when_absent() {
let mut argv = os(&["aube", "install", "--frozen-lockfile"]);
let parsed = extract_config_overrides(&mut argv);
assert!(parsed.is_empty());
assert_eq!(argv, os(&["aube", "install", "--frozen-lockfile"]));
}
}
#[cfg(test)]
mod package_manager_guard_tests {
use super::*;
#[test]
fn run_like_commands_warn_instead_of_erroring() {
let run = Cli::try_parse_from(["aube", "run", "test"]).expect("run should parse");
let test = Cli::try_parse_from(["aube", "test"]).expect("test should parse");
assert_eq!(
package_manager_guard_mode(run.command.as_ref()),
PackageManagerGuardMode::WarnAndSkipAutoInstall
);
assert_eq!(
package_manager_guard_mode(test.command.as_ref()),
PackageManagerGuardMode::WarnAndSkipAutoInstall
);
}
#[test]
fn install_still_errors_on_mismatch() {
let cli = Cli::try_parse_from(["aube", "install"]).expect("install should parse");
assert_eq!(
package_manager_guard_mode(cli.command.as_ref()),
PackageManagerGuardMode::Error
);
}
#[test]
fn install_test_still_errors_on_mismatch() {
let cli = Cli::try_parse_from(["aube", "install-test"]).expect("install-test should parse");
assert_eq!(
package_manager_guard_mode(cli.command.as_ref()),
PackageManagerGuardMode::Error
);
}
#[test]
fn package_manager_strict_mode_parses_canonical_spellings() {
for (input, expected) in [
("off", PackageManagerStrictMode::Off),
("warn", PackageManagerStrictMode::Warn),
("error", PackageManagerStrictMode::Error),
(" ERROR\n", PackageManagerStrictMode::Error),
] {
assert_eq!(PackageManagerStrictMode::parse(input), Some(expected));
}
}
#[test]
fn package_manager_strict_mode_parses_bool_back_compat() {
for (input, expected) in [
("true", PackageManagerStrictMode::Error),
("false", PackageManagerStrictMode::Off),
("1", PackageManagerStrictMode::Error),
("0", PackageManagerStrictMode::Off),
] {
assert_eq!(PackageManagerStrictMode::parse(input), Some(expected));
}
}
#[test]
fn package_manager_strict_mode_returns_none_for_typos() {
assert!(PackageManagerStrictMode::parse("errror").is_none());
assert!(PackageManagerStrictMode::parse("warning").is_none());
assert!(PackageManagerStrictMode::parse("").is_none());
}
}
#[cfg(test)]
mod cli_ordering_tests {
use super::*;
use clap::CommandFactory;
use std::collections::BTreeMap;
#[test]
fn test_cli_ordering() {
check_command_sorted(&Cli::command(), &[]);
}
fn check_command_sorted(cmd: &clap::Command, path: &[&str]) {
let mut current_path: Vec<&str> = path.to_vec();
current_path.push(cmd.get_name());
let names: Vec<_> = cmd.get_subcommands().map(|s| s.get_name()).collect();
let mut sorted = names.clone();
sorted.sort();
assert!(
names == sorted,
"Subcommands in '{}' are not sorted alphabetically!\nActual: {:?}\nExpected: {:?}",
current_path.join(" "),
names,
sorted,
);
let mut shorts: Vec<char> = Vec::new();
let mut by_heading: BTreeMap<Option<&str>, Vec<&str>> = BTreeMap::new();
for arg in cmd.get_arguments() {
if let Some(s) = arg.get_short() {
shorts.push(s);
} else if let Some(l) = arg.get_long() {
by_heading
.entry(arg.get_help_heading())
.or_default()
.push(l);
}
}
let mut sorted_shorts = shorts.clone();
sorted_shorts.sort_by_key(|c| (c.to_ascii_lowercase(), c.is_uppercase()));
assert!(
shorts == sorted_shorts,
"Short flags in '{}' are not sorted!\nActual: {:?}\nExpected: {:?}",
current_path.join(" "),
shorts,
sorted_shorts,
);
for (heading, longs) in &by_heading {
let mut sorted_longs = longs.clone();
sorted_longs.sort();
assert!(
longs == &sorted_longs,
"Long-only flags under heading {:?} in '{}' are not sorted!\nActual: {:?}\nExpected: {:?}",
heading,
current_path.join(" "),
longs,
sorted_longs,
);
}
for sub in cmd.get_subcommands() {
check_command_sorted(sub, ¤t_path);
}
}
}