1mod argv;
16pub mod cli_args;
17pub mod command_effects;
18pub mod commands;
19mod dep_chain;
20mod deprecations;
21mod dirs;
22pub mod embed;
23mod engines;
24mod patches;
25mod pnpmfile;
26mod process_guard;
27mod progress;
28mod runtime;
29mod self_version;
30mod startup;
31mod state;
32mod tool_shims;
33mod update_check;
34mod version;
35
36use argv::{extract_config_overrides, lift_per_subcommand_flags, rewrite_multicall_argv};
37use miette::{Context, IntoDiagnostic};
38use startup::{
39 ColorMode, PackageManagerGuard, ci_renders_ansi, command_needs_package_manager_guard,
40 compute_effective_filter, diag_config_from_flag, enforce_package_manager_guardrails,
41 env_disables_color, init_logging, load_startup_settings, raise_nofile_limit,
42 resolve_color_mode, resolve_loglevel, startup_cwd,
43};
44#[cfg(test)]
45use startup::{PackageManagerGuardMode, PackageManagerStrictMode, package_manager_guard_mode};
46use std::ffi::OsString;
47use std::path::PathBuf;
48
49#[derive(usage_rs::Cli)]
50#[allow(dead_code)]
51#[usage(
52 name = aube_util::prog(),
53 name_spec = "aube",
54 about = "A fast Node.js package manager",
55 view("aubr", root = "run", globals),
56 view("aubx", root = "dlx", globals)
57)]
58pub(crate) struct Cli {
59 #[usage(
61 short = 'C',
62 long = "dir",
63 long = "cd",
64 long = "prefix",
65 global,
66 value_name = "DIR"
67 )]
68 dir: Option<std::path::PathBuf>,
69
70 #[usage(short = 'F', long, global, value_name = "WORKSPACE")]
81 filter: Vec<String>,
82
83 #[usage(short = 'r', long, global)]
89 recursive: bool,
90
91 #[usage(short, long, global)]
93 verbose: bool,
94
95 #[usage(short = 'V', long = "version", global)]
101 version: bool,
102
103 #[usage(long, global, conflicts = "--stream", hide)]
108 aggregate_output: bool,
109
110 #[usage(long, global, conflicts = "--no-color")]
115 color: bool,
116
117 #[usage(long, global, value_name = "MODE", default_missing = "trace")]
127 diag: Option<String>,
128
129 #[usage(long, global, value_name = "PATH")]
131 diag_file: Option<PathBuf>,
132
133 #[usage(long, global, value_name = "MS")]
135 diag_threshold_ms: Option<u64>,
136
137 #[usage(long, global)]
141 fail_if_no_match: bool,
142
143 #[usage(long, global, value_name = "PATTERN")]
152 filter_prod: Vec<String>,
153
154 #[usage(long, global, hide)]
158 ignore_workspace: bool,
159
160 #[usage(long, global, hide)]
164 include_workspace_root: bool,
165
166 #[usage(long, global, value_name = "LEVEL", value_enum)]
168 loglevel: Option<LogLevel>,
169
170 #[usage(long, global)]
176 no_color: bool,
177
178 #[usage(long, global, value_name = "NAME", value_enum)]
187 reporter: Option<ReporterType>,
188
189 #[usage(long, global)]
191 silent: bool,
192
193 #[usage(long, global, conflicts = "--aggregate-output", hide)]
198 stream: bool,
199
200 #[usage(long, global, hide)]
204 use_stderr: bool,
205
206 #[usage(long, global, hide)]
211 workspace_packages: bool,
212
213 #[usage(long, global)]
215 workspace_root: bool,
216
217 #[usage(short = 'y', long, global, hide)]
222 yes: bool,
223
224 #[usage(subcommand)]
225 command: Option<Commands>,
226}
227
228#[cfg(test)]
229impl Cli {
230 fn try_parse_test_from<I, S>(argv: I) -> Result<Self, String>
231 where
232 I: IntoIterator<Item = S>,
233 S: Into<OsString>,
234 {
235 let raw: Vec<OsString> = argv.into_iter().map(Into::into).collect();
236 let argv: Vec<&std::ffi::OsStr> = raw.iter().map(OsString::as_os_str).collect();
237 Self::parse_from_argv(&argv)
238 .map_err(|error| render_cli_error(argv.get(1..).unwrap_or_default(), &error))
239 }
240}
241
242fn render_cli_error(argv: &[&std::ffi::OsStr], error: &usage_rs::Error<'_, '_>) -> String {
243 if let usage_rs::Error::MissingFlagValue { flag } = error
244 && flag.require_equals
245 && flag.longs.contains(&"allow-build")
246 {
247 return "error: equal sign is needed when assigning values to '--allow-build=<PKG>'"
248 .to_owned();
249 }
250 if let usage_rs::Error::InvalidValue(detail) = error
251 && detail
252 .reason
253 .starts_with("The --allow-build flag is missing a package name.")
254 {
255 return detail
256 .reason
257 .replacen(" Please specify", "\nPlease specify", 1);
258 }
259 let runtime_spec = Cli::runtime_app().spec();
260 usage_rs::render_failure(&runtime_spec, argv, error)
261}
262
263fn is_allow_build_compat_error(error: &usage_rs::Error<'_, '_>) -> bool {
264 matches!(
265 error,
266 usage_rs::Error::MissingFlagValue { flag }
267 if flag.require_equals && flag.longs.contains(&"allow-build")
268 ) || matches!(
269 error,
270 usage_rs::Error::InvalidValue(detail)
271 if detail.reason.starts_with("The --allow-build flag is missing a package name.")
272 )
273}
274
275#[derive(Copy, Clone, Debug, PartialEq, Eq, usage_rs::ValueEnum, strum::EnumString)]
276#[strum(serialize_all = "kebab-case")]
277pub(crate) enum LogLevel {
278 Trace,
279 Debug,
280 Info,
281 Warn,
282 Error,
283 Silent,
284}
285
286#[derive(Copy, Clone, Debug, PartialEq, Eq, usage_rs::ValueEnum, strum::EnumString)]
287#[strum(serialize_all = "kebab-case")]
288pub(crate) enum ReporterType {
289 Default,
290 AppendOnly,
291 Ndjson,
292 Silent,
293}
294
295impl LogLevel {
296 fn filter(self) -> &'static str {
297 match self {
298 LogLevel::Trace => "trace",
299 LogLevel::Debug => "debug",
300 LogLevel::Info => "info",
301 LogLevel::Warn => "warn",
302 LogLevel::Error => "error",
303 LogLevel::Silent => "off",
304 }
305 }
306}
307
308struct SilentStderrGuard {
316 saved: libc::c_int,
317}
318
319impl SilentStderrGuard {
320 fn install() -> Option<Self> {
321 unsafe {
322 let saved = libc::dup(2);
323 if saved < 0 {
324 return None;
325 }
326 let devnull = libc::open(c"/dev/null".as_ptr(), libc::O_WRONLY);
327 if devnull < 0 {
328 libc::close(saved);
329 return None;
330 }
331 if libc::dup2(devnull, 2) < 0 {
332 libc::close(devnull);
333 libc::close(saved);
334 return None;
335 }
336 libc::close(devnull);
337 Some(Self { saved })
338 }
339 }
340}
341
342impl Drop for SilentStderrGuard {
343 fn drop(&mut self) {
344 unsafe {
345 libc::dup2(self.saved, 2);
346 libc::close(self.saved);
347 }
348 }
349}
350
351#[derive(usage_rs::Subcommands)]
358enum Commands {
359 #[usage(name = "__node-gyp-bootstrap", hide)]
361 NodeGypBootstrap { project_dir: PathBuf },
362 Access(commands::access::AccessArgs),
364 Activate(commands::activate::ActivateArgs),
366 #[usage(alias = "a")]
368 Add(commands::add::AddArgs),
369 ApproveBuilds(commands::approve_builds::ApproveBuildsArgs),
374 Audit(commands::audit::AuditArgs),
376 Bin(commands::bin::BinArgs),
378 #[usage(alias = "issues", after_long_help = commands::bugs::AFTER_LONG_HELP)]
380 Bugs(commands::bugs::BugsArgs),
381 Cache(commands::cache::CacheArgs),
383 CatFile(commands::cat_file::CatFileArgs),
385 CatIndex(commands::cat_index::CatIndexArgs),
387 Check(commands::check::CheckArgs),
392 #[usage(alias = "clean-install", alias("ic", "install-clean"))]
396 Ci(commands::ci::CiArgs),
397 Clean(commands::clean::CleanArgs),
402 Completion(commands::completion::CompletionArgs),
404 #[usage(alias = "c")]
406 Config(commands::config::ConfigArgs),
407 Create(commands::create::CreateArgs),
409 Dedupe(commands::dedupe::DedupeArgs),
411 Deploy(commands::deploy::DeployArgs),
413 Deprecate(commands::deprecate::DeprecateArgs),
415 Deprecations(commands::deprecations::DeprecationsArgs),
417 Diag(commands::diag::DiagArgs),
419 #[usage(alias = "dist-tags")]
421 DistTag(commands::dist_tag::DistTagArgs),
422 Dlx(commands::dlx::DlxArgs),
424 Doctor(commands::doctor::DoctorArgs),
426 #[usage(alias = "x")]
428 Exec(commands::exec::ExecArgs),
429 Fetch(commands::fetch::FetchArgs),
431 FindHash(commands::find_hash::FindHashArgs),
433 #[usage(hide)]
435 Get(commands::config::GetArgs),
436 IgnoredBuilds(commands::ignored_builds::IgnoredBuildsArgs),
438 Import(commands::import::ImportArgs),
440 Init(commands::init::InitArgs),
442 #[usage(alias = "i")]
444 Install(commands::install::InstallArgs),
445 #[usage(alias = "it", hide)]
449 InstallTest(commands::run::InstallTestArgs),
450 #[usage(hide)]
452 La(commands::list::LaArgs),
453 Licenses(commands::licenses::LicensesArgs),
455 #[usage(alias = "ln")]
457 Link(commands::link::LinkArgs),
458 #[usage(alias = "ls", after_long_help = commands::list::AFTER_LONG_HELP)]
460 List(commands::list::ListArgs),
461 #[usage(hide)]
463 Ll(commands::list::LlArgs),
464 #[usage(alias = "adduser")]
466 Login(commands::login::LoginArgs),
467 Logout(commands::logout::LogoutArgs),
469 Node(commands::node::NodeArgs),
471 Outdated(commands::outdated::OutdatedArgs),
473 #[usage(hide)]
475 Owner(commands::npm_fallback::OwnerArgs),
476 Pack(commands::pack::PackArgs),
478 Patch(commands::patch::PatchArgs),
480 PatchCommit(commands::patch_commit::PatchCommitArgs),
482 PatchRemove(commands::patch_remove::PatchRemoveArgs),
484 Peers(commands::peers::PeersArgs),
486 #[usage(hide)]
488 Pkg(commands::npm_fallback::PkgArgs),
489 Prefix(commands::prefix::PrefixArgs),
491 Prune(commands::prune::PruneArgs),
497 #[cfg(feature = "publish")]
499 Publish(commands::publish::PublishArgs),
500 Purge(commands::clean::PurgeArgs),
504 Query(commands::query::QueryArgs),
506 #[usage(alias = "rb")]
508 Rebuild(commands::rebuild::RebuildArgs),
509 #[usage(alias("multi", "m"))]
511 Recursive(commands::recursive::RecursiveArgs),
512 #[usage(alias = "rm", alias("uninstall", "un", "uni"))]
514 Remove(commands::remove::RemoveArgs),
515 Restart(commands::run::RestartArgs),
517 Root(commands::root::RootArgs),
519 #[usage(alias = "run-script")]
521 Run(commands::run::RunArgs),
522 #[usage(alias = "rt")]
524 Runtime(commands::runtime::RuntimeArgs),
525 Sbom(commands::sbom::SbomArgs),
527 #[usage(hide)]
529 Search(commands::npm_fallback::SearchArgs),
530 #[usage(hide)]
532 Set(commands::config::SetArgs),
533 #[usage(hide, name = "set-script")]
535 SetScript(commands::npm_fallback::SetScriptArgs),
536 Sponsors(commands::sponsors::SponsorsArgs),
538 Stage(commands::npm_fallback::StageArgs),
540 Start(commands::run::StartArgs),
542 Stop(commands::run::StopArgs),
544 Store(commands::store::StoreArgs),
546 #[usage(alias = "t")]
548 Test(commands::run::TestArgs),
549 #[usage(hide)]
551 Token(commands::npm_fallback::TokenArgs),
552 Trust(commands::trust::TrustArgs),
554 Undeprecate(commands::undeprecate::UndeprecateArgs),
556 #[usage(alias = "dislink")]
558 Unlink(commands::unlink::UnlinkArgs),
559 Unpublish(commands::unpublish::UnpublishArgs),
561 #[usage(alias("up", "upgrade"))]
563 Update(commands::update::UpdateArgs),
564 Version(commands::version::VersionArgs),
566 #[usage(alias("info", "show"), alias = "v", after_long_help = commands::view::AFTER_LONG_HELP)]
568 View(commands::view::ViewArgs),
569 #[usage(hide)]
571 Whoami(commands::npm_fallback::WhoamiArgs),
572 #[usage(alias = "w", after_long_help = commands::why::AFTER_LONG_HELP)]
574 Why(commands::why::WhyArgs),
575 #[usage(external_subcommand)]
576 External(Vec<String>),
577}
578
579#[must_use]
598pub fn cli_main(embedder: &'static aube_util::Embedder) -> i32 {
599 cli_main_with_defaults(embedder, Vec::new())
600}
601
602pub fn command() -> &'static usage_rs::Command<'static> {
607 Cli::command()
608}
609
610pub fn spec() -> &'static usage_rs::spec::Spec<'static> {
612 Cli::spec()
613}
614
615pub fn usage_kdl() -> String {
616 let overlays = command_effects::overlays();
617 Cli::app().overlay(&overlays).to_kdl()
618}
619
620pub fn usage_kdl_for(name: &'static str) -> String {
621 let overlays = command_effects::overlays();
622 Cli::app().name(name).bin(name).overlay(&overlays).to_kdl()
623}
624
625pub fn completion_app(name: &'static str) -> usage_rs::complete::App<'static> {
626 let mut app = Cli::app()
627 .name(name)
628 .bin(name)
629 .completion_app()
630 .completions(&commands::completion::COMPLETIONS);
631 if let Some(view) = Cli::spec().views.iter().find(|view| view.bin == name) {
632 app = app.project(view.root);
633 }
634 app
635}
636
637fn print_subcommand_help(name: &str) -> miette::Result<()> {
638 let command = Cli::command()
639 .subcommands
640 .iter()
641 .copied()
642 .find(|command| command.name == name)
643 .ok_or_else(|| miette::miette!("unknown subcommand {name:?}"))?;
644 let page = usage_rs::help::render(Cli::spec(), command, true)
645 .ok_or_else(|| miette::miette!("failed to render help for {name:?}"))?;
646 print!("{page}");
647 Ok(())
648}
649
650#[must_use]
661pub fn cli_main_with_defaults(
662 embedder: &'static aube_util::Embedder,
663 defaults: Vec<(String, String)>,
664) -> i32 {
665 aube_util::set_embedder(embedder);
670 aube_settings::set_embedder_defaults(defaults);
671
672 let prev_hook = std::panic::take_hook();
684 std::panic::set_hook(Box::new(move |info| {
685 aube_util::diag::flush();
686 prev_hook(info);
687 }));
688 let result = inner_main();
689 aube_util::diag::flush();
690 aube_registry::slow_metadata::flush_summary();
697 match result {
703 Ok(code) => code,
704 Err(report) => {
705 eprintln!("{report:?}");
706 report_exit_code(&report)
707 }
708 }
709}
710
711fn report_exit_code(report: &miette::Report) -> i32 {
715 if let Some(code) = report.code() {
716 let code = code.to_string();
717 if let Some(exit) = aube_codes::exit::exit_code_for(&code) {
718 return exit;
719 }
720 }
721 aube_codes::exit::EXIT_GENERIC
722}
723
724fn inner_main() -> miette::Result<i32> {
725 let mut argv: Vec<OsString> = std::env::args_os().collect();
726 let invoked_as_aubr = argv
727 .first()
728 .is_some_and(|arg| crate::tool_shims::stem_of_argv0(arg) == "aubr");
729 if argv.get(1).and_then(|arg| arg.to_str()) == Some("__complete_word__") {
730 let name = argv
731 .first()
732 .and_then(|arg| std::path::Path::new(arg).file_stem())
733 .and_then(|name| name.to_str())
734 .and_then(|name| match name {
735 "aubr" => Some("aubr"),
736 "aubx" => Some("aubx"),
737 _ => None,
738 })
739 .unwrap_or("aube");
740 let runtime = tokio::runtime::Builder::new_current_thread()
741 .enable_all()
742 .build()
743 .into_diagnostic()
744 .wrap_err("failed to build completion runtime")?;
745 if let Some(answer) = runtime.block_on(completion_app(name).completion_request(&argv[1..]))
746 {
747 print!("{answer}");
748 }
749 return Ok(0);
750 }
751 let config_overrides = extract_config_overrides(&mut argv);
756 aube_settings::set_global_cli_overrides(config_overrides);
757 tool_shims::sanitize_process_path();
762 let argv = lift_per_subcommand_flags(rewrite_multicall_argv(argv));
763 let argv: Vec<&std::ffi::OsStr> = argv.iter().map(OsString::as_os_str).collect();
764 let cli = match Cli::parse_from_argv(&argv) {
765 Ok(cli) => cli,
766 Err(usage_rs::Error::Version { .. }) => {
767 println!(
768 "{} {}",
769 aube_util::embedder().name,
770 env!("CARGO_PKG_VERSION")
771 );
772 return Ok(0);
773 }
774 Err(usage_rs::Error::Help { cmd, long }) => {
775 let runtime_spec = Cli::runtime_app().spec();
776 if let Some(page) = usage_rs::help::render(&runtime_spec, cmd, long) {
777 print!("{page}");
778 }
779 return Ok(0);
780 }
781 Err(error) => {
782 let raw_compat_error = is_allow_build_compat_error(&error);
783 let message = render_cli_error(argv.get(1..).unwrap_or_default(), &error);
784 if raw_compat_error {
785 eprintln!("{message}");
786 return Ok(aube_codes::exit::EXIT_GENERIC);
787 }
788 return Err(miette::miette!("{message}"));
789 }
790 };
791
792 if let Some(Commands::Run(args)) = cli.command.as_ref()
809 && args.complete
810 {
811 commands::run::print_script_completions(cli.dir.as_deref());
812 return Ok(0);
813 }
814 let color_mode = resolve_color_mode(&cli);
827 if matches!(color_mode, ColorMode::Never) {
828 unsafe {
830 std::env::set_var("NO_COLOR", "1");
831 std::env::remove_var("FORCE_COLOR");
832 std::env::remove_var("CLICOLOR_FORCE");
833 }
834 } else if matches!(color_mode, ColorMode::Always) {
835 unsafe {
837 std::env::set_var("FORCE_COLOR", "1");
838 std::env::set_var("CLICOLOR_FORCE", "1");
839 std::env::remove_var("NO_COLOR");
840 }
841 } else if ci_renders_ansi() && !env_disables_color() {
842 console::set_colors_enabled_stderr(true);
852 }
853
854 let is_silent = cli.silent || matches!(cli.reporter, Some(ReporterType::Silent));
862 if !is_silent {
863 let use_stderr_active = cli.use_stderr
864 || startup_cwd(&cli).ok().is_some_and(|cwd| {
865 let files = commands::FileSources::load(&cwd);
866 let ws = std::collections::BTreeMap::new();
867 let env_snap = aube_settings::values::capture_env();
868 aube_settings::resolved::use_stderr(&files.ctx(&ws, &env_snap, &[]))
869 });
870 if use_stderr_active {
871 unsafe {
874 libc::dup2(2, 1);
875 }
876 }
877 }
878
879 let parse_env = |key: &str, default: usize| -> usize {
896 std::env::var(key)
897 .ok()
898 .and_then(|s| s.parse::<usize>().ok())
899 .filter(|n| *n > 0)
900 .unwrap_or(default)
901 };
902 let cpu_count = std::thread::available_parallelism()
903 .map(|n| n.get())
904 .unwrap_or(4);
905 let workers = parse_env("AUBE_TOKIO_WORKERS", cpu_count.min(8));
906 let blocking = parse_env("AUBE_TOKIO_BLOCKING", 128);
907 let current_thread_run = aubr_uses_current_thread(invoked_as_aubr, &cli);
911 let mut runtime_builder = if current_thread_run {
912 tokio::runtime::Builder::new_current_thread()
913 } else {
914 let mut builder = tokio::runtime::Builder::new_multi_thread();
915 builder.worker_threads(workers);
916 builder
917 };
918 let runtime = runtime_builder
919 .max_blocking_threads(blocking)
920 .enable_all()
921 .build()
922 .into_diagnostic()
923 .wrap_err("failed to build tokio runtime")?;
924 let exit_code = if current_thread_run {
925 runtime.block_on(commands::with_lazy_install_runtime(
926 commands::LazyInstallRuntime::new(workers, blocking),
927 async_main(cli, invoked_as_aubr),
928 ))?
929 } else {
930 runtime.block_on(async_main(cli, invoked_as_aubr))?
931 };
932 drop(runtime);
933 Ok(exit_code.unwrap_or(0))
940}
941
942fn aubr_uses_current_thread(invoked_as_aubr: bool, cli: &Cli) -> bool {
943 invoked_as_aubr && matches!(cli.command.as_ref(), Some(Commands::Run(_)))
944}
945
946async fn async_main(cli: Cli, invoked_as_aubr: bool) -> miette::Result<Option<i32>> {
947 if let Some(dir) = &cli.dir {
956 std::env::set_current_dir(dir)
957 .into_diagnostic()
958 .wrap_err_with(|| format!("failed to change directory to {}", dir.display()))?;
959 }
960
961 let print_top_level_version = should_print_top_level_version(&cli);
962 if cli.workspace_root && !print_top_level_version {
963 let start = std::env::current_dir()
964 .into_diagnostic()
965 .wrap_err("failed to read current dir")?;
966 let root = commands::find_workspace_root(&start)?;
967 if root != start {
968 std::env::set_current_dir(&root)
969 .into_diagnostic()
970 .wrap_err_with(|| format!("failed to change directory to {}", root.display()))?;
971 }
972 crate::dirs::set_cwd(&root)?;
973 }
974
975 let settings = load_startup_settings()?;
976 let effective_level = resolve_loglevel(&cli, settings.loglevel.as_deref());
977 init_logging(&cli, effective_level);
978
979 let _silent_guard = matches!(effective_level, LogLevel::Silent)
990 .then(SilentStderrGuard::install)
991 .flatten();
992 if let Some(ref guard) = _silent_guard {
993 aube_scripts::set_saved_stderr_fd(guard.saved);
994 }
995
996 if print_top_level_version {
997 self_version::maybe_switch(&settings).await?;
998 println!("{}", crate::version::VERSION_LONG.as_str());
999 let cwd =
1000 crate::dirs::project_root_or_cwd().unwrap_or_else(|_| std::path::PathBuf::from("."));
1001 update_check::check_and_notify(&cwd).await;
1002 return Ok(None);
1003 }
1004
1005 if !matches!(cli.command, Some(Commands::Diag(_))) {
1008 match diag_config_from_flag(&cli) {
1009 Some(cfg_opt) => aube_util::diag::init_with_config(cfg_opt),
1010 None => aube_util::diag::init(),
1011 }
1012 }
1013 raise_nofile_limit();
1014
1015 commands::set_skip_auto_install_on_package_manager_mismatch(false);
1016 if command_needs_package_manager_guard(cli.command.as_ref()) {
1017 self_version::maybe_switch(&settings).await?;
1022 let guard = enforce_package_manager_guardrails(&settings, cli.command.as_ref())?;
1023 commands::set_skip_auto_install_on_package_manager_mismatch(
1024 guard == PackageManagerGuard::WarnRunOnly,
1025 );
1026 }
1027
1028 let effective_filter = compute_effective_filter(&cli);
1031
1032 commands::set_global_output_flags(commands::GlobalOutputFlags {
1033 ndjson: matches!(cli.reporter, Some(ReporterType::Ndjson)),
1034 silent: matches!(effective_level, LogLevel::Silent),
1035 });
1036
1037 match cli.command {
1038 Some(Commands::NodeGypBootstrap { project_dir }) => {
1039 let binary = embed::bootstrap_node_gyp(&project_dir).await?;
1040 println!("{}", binary.display());
1041 }
1042 Some(Commands::Access(args)) => commands::access::run(args).await?,
1043 Some(Commands::Activate(args)) => commands::activate::run(args)?,
1044 Some(Commands::Add(args)) => {
1045 commands::add::run(args, effective_filter.clone()).await?;
1046 }
1047 Some(Commands::ApproveBuilds(args)) => commands::approve_builds::run(args).await?,
1048 Some(Commands::Audit(args)) => {
1049 if let Some(code) = commands::audit::run(args).await? {
1050 return Ok(Some(code));
1051 }
1052 }
1053 Some(Commands::Bin(args)) => commands::bin::run(args).await?,
1054 Some(Commands::Bugs(args)) => commands::bugs::run(args).await?,
1055 Some(Commands::Cache(args)) => commands::cache::run(args).await?,
1056 Some(Commands::CatFile(args)) => commands::cat_file::run(args).await?,
1057 Some(Commands::CatIndex(args)) => commands::cat_index::run(args).await?,
1058 Some(Commands::Check(args)) => {
1059 if let Some(code) = commands::check::run(args).await? {
1060 return Ok(Some(code));
1061 }
1062 }
1063 Some(Commands::Ci(args)) => commands::ci::run(args).await?,
1064 Some(Commands::Clean(args)) => {
1065 if let Some(code) = commands::clean::run(args).await? {
1066 return Ok(Some(code));
1067 }
1068 }
1069 Some(Commands::Completion(args)) => commands::completion::run(args).await?,
1070 Some(Commands::Config(args)) => commands::config::run(args).await?,
1071 Some(Commands::Create(args)) => {
1072 if let Some(code) = commands::create::run(args).await? {
1073 return Ok(Some(code));
1074 }
1075 }
1076 Some(Commands::Dedupe(args)) => commands::dedupe::run(args).await?,
1077 Some(Commands::Deploy(args)) => {
1078 commands::deploy::run(args, effective_filter.clone()).await?
1079 }
1080 Some(Commands::Deprecate(args)) => commands::deprecate::run(args).await?,
1081 Some(Commands::Deprecations(args)) => {
1082 if let Some(code) = commands::deprecations::run(args).await? {
1083 return Ok(Some(code));
1084 }
1085 }
1086 Some(Commands::Diag(args)) => commands::diag::run(args).await?,
1087 Some(Commands::DistTag(args)) => commands::dist_tag::run(args).await?,
1088 Some(Commands::Dlx(args)) => {
1089 if let Some(code) = commands::dlx::run(args).await? {
1090 return Ok(Some(code));
1091 }
1092 }
1093 Some(Commands::Doctor(args)) => {
1094 if let Some(code) = commands::doctor::run(args).await? {
1095 return Ok(Some(code));
1096 }
1097 }
1098 Some(Commands::Exec(args)) => {
1099 if let Some(code) = commands::exec::run(args, effective_filter.clone()).await? {
1100 return Ok(Some(code));
1101 }
1102 }
1103 Some(Commands::Fetch(args)) => commands::fetch::run(args).await?,
1104 Some(Commands::FindHash(args)) => commands::find_hash::run(args).await?,
1105 Some(Commands::Get(args)) => commands::config::get(args)?,
1106 Some(Commands::IgnoredBuilds(args)) => commands::ignored_builds::run(args).await?,
1107 Some(Commands::Import(args)) => commands::import::run(args).await?,
1108 Some(Commands::Init(args)) => commands::init::run(args).await?,
1109 Some(Commands::Install(args)) => {
1110 run_install_command(args, effective_filter.clone(), cli.workspace_root).await?;
1111 }
1112 Some(Commands::InstallTest(args)) => {
1113 if let Some(code) = commands::install_test::run(args.into_inner()).await? {
1114 return Ok(Some(code));
1115 }
1116 }
1117 Some(Commands::La(commands::list::LaArgs { mut args }))
1118 | Some(Commands::Ll(commands::list::LlArgs { mut args })) => {
1119 args.long = true;
1120 commands::list::run(args, effective_filter.clone()).await?;
1121 }
1122 Some(Commands::Licenses(args)) => commands::licenses::run(args).await?,
1123 Some(Commands::Link(args)) => commands::link::run(args).await?,
1124 Some(Commands::List(args)) => commands::list::run(args, effective_filter.clone()).await?,
1125 Some(Commands::Login(args)) => commands::login::run(args).await?,
1126 Some(Commands::Logout(args)) => commands::logout::run(args).await?,
1127 Some(Commands::Node(args)) => {
1128 if let Some(code) = commands::node::run(args).await? {
1129 return Ok(Some(code));
1130 }
1131 }
1132 Some(Commands::Outdated(args)) => {
1133 if let Some(code) = commands::outdated::run(args, effective_filter.clone()).await? {
1134 return Ok(Some(code));
1135 }
1136 }
1137 Some(Commands::Owner(args)) => {
1138 return Ok(Some(commands::npm_fallback::run("owner", &args)?));
1139 }
1140 Some(Commands::Pack(args)) => commands::pack::run(args).await?,
1141 Some(Commands::Patch(args)) => commands::patch::run(args).await?,
1142 Some(Commands::PatchCommit(args)) => commands::patch_commit::run(args).await?,
1143 Some(Commands::PatchRemove(args)) => commands::patch_remove::run(args).await?,
1144 Some(Commands::Peers(args)) => {
1145 if let Some(code) = commands::peers::run(args).await? {
1146 return Ok(Some(code));
1147 }
1148 }
1149 Some(Commands::Pkg(args)) => {
1150 return Ok(Some(commands::npm_fallback::run("pkg", &args)?));
1151 }
1152 Some(Commands::Prefix(args)) => commands::prefix::run(args).await?,
1153 Some(Commands::Prune(args)) => commands::prune::run(args).await?,
1154 #[cfg(feature = "publish")]
1155 Some(Commands::Publish(args)) => {
1156 commands::publish::run(args, effective_filter.clone()).await?
1157 }
1158 Some(Commands::Purge(args)) => {
1159 if let Some(code) = commands::clean::run_purge(args.inner).await? {
1160 return Ok(Some(code));
1161 }
1162 }
1163 Some(Commands::Query(args)) => commands::query::run(args, effective_filter.clone()).await?,
1164 Some(Commands::Rebuild(args)) => {
1165 commands::rebuild::run(args, effective_filter.clone()).await?
1166 }
1167 Some(Commands::Remove(args)) => {
1168 commands::remove::run(args, effective_filter.clone()).await?
1169 }
1170 Some(Commands::Recursive(args)) => {
1171 let argv = commands::recursive::argv(
1172 args,
1173 commands::recursive::RecursiveGlobals {
1174 filters: effective_filter.clone(),
1175 color: cli.color,
1176 no_color: cli.no_color,
1177 },
1178 )?;
1179 let nested_argv: Vec<OsString> =
1185 lift_per_subcommand_flags(argv.into_iter().map(OsString::from).collect());
1186 let nested_refs: Vec<&std::ffi::OsStr> =
1187 nested_argv.iter().map(OsString::as_os_str).collect();
1188 let nested = Cli::parse_from_argv(&nested_refs).map_err(|error| {
1189 miette::miette!(
1190 "{}",
1191 usage_rs::render_failure(
1192 Cli::spec(),
1193 nested_refs.get(1..).unwrap_or_default(),
1194 &error,
1195 )
1196 )
1197 })?;
1198 let nested_filter = compute_effective_filter(&nested);
1199 match nested.command {
1200 Some(Commands::Add(args)) => {
1201 commands::add::run(args, nested_filter).await?;
1202 }
1203 Some(Commands::Deploy(args)) => commands::deploy::run(args, nested_filter).await?,
1204 Some(Commands::Exec(args)) => {
1205 if let Some(code) = commands::exec::run(args, nested_filter).await? {
1206 return Ok(Some(code));
1207 }
1208 }
1209 Some(Commands::Install(args)) => {
1210 run_install_command(args, nested_filter, nested.workspace_root).await?;
1211 }
1212 Some(Commands::List(args)) => commands::list::run(args, nested_filter).await?,
1213 Some(Commands::La(commands::list::LaArgs { mut args }))
1214 | Some(Commands::Ll(commands::list::LlArgs { mut args })) => {
1215 args.long = true;
1216 commands::list::run(args, nested_filter).await?;
1217 }
1218 Some(Commands::Outdated(args)) => {
1219 if let Some(code) = commands::outdated::run(args, nested_filter).await? {
1220 return Ok(Some(code));
1221 }
1222 }
1223 #[cfg(feature = "publish")]
1224 Some(Commands::Publish(args)) => {
1225 commands::publish::run(args, nested_filter).await?
1226 }
1227 Some(Commands::Rebuild(args)) => {
1228 commands::rebuild::run(args, nested_filter).await?
1229 }
1230 Some(Commands::Remove(args)) => commands::remove::run(args, nested_filter).await?,
1231 Some(Commands::Restart(args)) => {
1232 if let Some(code) =
1233 commands::restart::run(args.into_inner(), nested_filter).await?
1234 {
1235 return Ok(Some(code));
1236 }
1237 }
1238 Some(Commands::Run(args)) => {
1239 if let Some(code) = commands::run::run(args, nested_filter).await? {
1240 return Ok(Some(code));
1241 }
1242 }
1243 Some(Commands::Start(args)) => {
1244 if let Some(code) =
1245 run_script_lifecycle("start", args.into_inner(), &nested_filter).await?
1246 {
1247 return Ok(Some(code));
1248 }
1249 }
1250 Some(Commands::Stop(args)) => {
1251 if let Some(code) =
1252 run_script_lifecycle("stop", args.into_inner(), &nested_filter).await?
1253 {
1254 return Ok(Some(code));
1255 }
1256 }
1257 Some(Commands::Test(args)) => {
1258 if let Some(code) =
1259 run_script_lifecycle("test", args.into_inner(), &nested_filter).await?
1260 {
1261 return Ok(Some(code));
1262 }
1263 }
1264 Some(Commands::Update(args)) => {
1265 if let Some(code) = commands::update::run(args, nested_filter).await? {
1266 return Ok(Some(code));
1267 }
1268 }
1269 Some(Commands::Why(args)) => commands::why::run(args, nested_filter).await?,
1270 Some(Commands::External(args)) => {
1271 let script = &args[0];
1272 let script_args: Vec<String> = args[1..].to_vec();
1273 if let Some(code) = commands::run::run_script(
1274 script,
1275 &script_args,
1276 false,
1277 false,
1278 &nested_filter,
1279 )
1280 .await?
1281 {
1282 return Ok(Some(code));
1283 }
1284 }
1285 Some(_) | None => {
1286 return Err(miette::miette!(
1287 code = aube_codes::errors::ERR_AUBE_RECURSIVE_NOT_SUPPORTED,
1288 "{} recursive: command does not support recursive execution",
1289 aube_util::embedder().name,
1290 ));
1291 }
1292 }
1293 }
1294 Some(Commands::Restart(args)) => {
1295 if let Some(code) =
1296 commands::restart::run(args.into_inner(), effective_filter.clone()).await?
1297 {
1298 return Ok(Some(code));
1299 }
1300 }
1301 Some(Commands::Root(args)) => commands::root::run(args).await?,
1302 Some(Commands::Run(args)) => {
1303 if let Some(code) = commands::run::run_with_process_replacement(
1304 args,
1305 effective_filter.clone(),
1306 invoked_as_aubr,
1307 )
1308 .await?
1309 {
1310 return Ok(Some(code));
1311 }
1312 }
1313 Some(Commands::Runtime(args)) => commands::runtime::run(args).await?,
1314 Some(Commands::Sbom(args)) => commands::sbom::run(args).await?,
1315 Some(Commands::Search(args)) => {
1316 return Ok(Some(commands::npm_fallback::run("search", &args)?));
1317 }
1318 Some(Commands::Set(args)) => commands::config::set(args)?,
1319 Some(Commands::SetScript(args)) => {
1320 return Ok(Some(commands::npm_fallback::run("set-script", &args)?));
1321 }
1322 Some(Commands::Sponsors(args)) => commands::sponsors::run(args).await?,
1323 Some(Commands::Stage(args)) => {
1324 return Ok(Some(commands::npm_fallback::run("stage", &args)?));
1325 }
1326 Some(Commands::Start(args)) => {
1327 if let Some(code) =
1328 run_script_lifecycle("start", args.into_inner(), &effective_filter).await?
1329 {
1330 return Ok(Some(code));
1331 }
1332 }
1333 Some(Commands::Stop(args)) => {
1334 if let Some(code) =
1335 run_script_lifecycle("stop", args.into_inner(), &effective_filter).await?
1336 {
1337 return Ok(Some(code));
1338 }
1339 }
1340 Some(Commands::Store(args)) => commands::store::run(args).await?,
1341 Some(Commands::Test(args)) => {
1342 if let Some(code) =
1343 run_script_lifecycle("test", args.into_inner(), &effective_filter).await?
1344 {
1345 return Ok(Some(code));
1346 }
1347 }
1348 Some(Commands::Token(args)) => {
1349 return Ok(Some(commands::npm_fallback::run("token", &args)?));
1350 }
1351 Some(Commands::Trust(args)) => commands::trust::run(args).await?,
1352 Some(Commands::Undeprecate(args)) => commands::undeprecate::run(args).await?,
1353 Some(Commands::Unlink(args)) => commands::unlink::run(args).await?,
1354 Some(Commands::Unpublish(args)) => commands::unpublish::run(args).await?,
1355 Some(Commands::Update(args)) => {
1356 if let Some(code) = commands::update::run(args, effective_filter.clone()).await? {
1357 return Ok(Some(code));
1358 }
1359 }
1360 Some(Commands::Version(args)) => commands::version::run(args).await?,
1361 Some(Commands::View(args)) => commands::view::run(args).await?,
1362 Some(Commands::Whoami(args)) => {
1363 return Ok(Some(commands::npm_fallback::run("whoami", &args)?));
1364 }
1365 Some(Commands::Why(args)) => commands::why::run(args, effective_filter.clone()).await?,
1366 Some(Commands::External(args)) => {
1367 let script = &args[0];
1385 let script_args: Vec<String> = args[1..].to_vec();
1386 if effective_filter.is_empty() {
1387 let initial_cwd = crate::dirs::cwd()?;
1388 let script_exists = crate::dirs::find_project_root(&initial_cwd)
1389 .and_then(|cwd| {
1390 aube_manifest::PackageJson::from_path(&cwd.join("package.json")).ok()
1391 })
1392 .map(|m| m.scripts.contains_key(script))
1393 .unwrap_or(false);
1394 if !script_exists {
1395 if let Some(page) = usage_rs::help::render(Cli::spec(), Cli::command(), true) {
1396 print!("{page}");
1397 }
1398 eprintln!();
1399 return Err(miette::miette!(
1400 code = aube_codes::errors::ERR_AUBE_UNKNOWN_COMMAND,
1401 "unknown command: {script}"
1402 ));
1403 }
1404 }
1405 if let Some(code) =
1406 commands::run::run_script(script, &script_args, false, false, &effective_filter)
1407 .await?
1408 {
1409 return Ok(Some(code));
1410 }
1411 }
1412 None => {
1413 if let Some(page) = usage_rs::help::render(Cli::spec(), Cli::command(), true) {
1417 print!("{page}");
1418 }
1419 }
1420 }
1421
1422 Ok(None)
1423}
1424
1425fn should_print_top_level_version(cli: &Cli) -> bool {
1426 cli.version && !matches!(cli.command, Some(Commands::Node(_)))
1427}
1428
1429async fn run_script_lifecycle(
1438 name: &str,
1439 args: commands::run::ScriptArgs,
1440 filter: &aube_workspace::selector::EffectiveFilter,
1441) -> miette::Result<Option<i32>> {
1442 args.network.install_overrides();
1443 args.lockfile.install_overrides();
1444 args.virtual_store.install_overrides();
1445 commands::run::run_script(name, &args.args, args.no_install, false, filter).await
1446}
1447
1448async fn run_install_command(
1449 args: commands::install::InstallArgs,
1450 filter: aube_workspace::selector::EffectiveFilter,
1451 workspace_root_already: bool,
1452) -> miette::Result<()> {
1453 if args.workspace_root_short && !workspace_root_already {
1457 let start = std::env::current_dir()
1458 .into_diagnostic()
1459 .wrap_err("failed to read current dir")?;
1460 let root = commands::find_workspace_root(&start)?;
1461 if root != start {
1462 std::env::set_current_dir(&root)
1463 .into_diagnostic()
1464 .wrap_err_with(|| format!("failed to change directory to {}", root.display()))?;
1465 }
1466 crate::dirs::set_cwd(&root)?;
1467 }
1468 args.network.install_overrides();
1469 args.lockfile.install_overrides();
1470 args.virtual_store.install_overrides();
1471 let global_frozen = args.lockfile.frozen_override();
1472 let global_gvs = args.virtual_store.flags();
1473 let cwd = crate::dirs::workspace_or_project_root()?;
1479 let files = commands::FileSources::load(&cwd);
1480 let raw_ws = aube_manifest::workspace::load_raw(&cwd)
1481 .into_diagnostic()
1482 .wrap_err("failed to load workspace config")?;
1483 let env = aube_settings::values::capture_env();
1484 let cli_flags = args.to_cli_flag_bag(global_frozen, global_gvs);
1485 let ctx = files.ctx(&raw_ws, &env, &cli_flags);
1486 let yaml_prefer_frozen = aube_settings::resolved::prefer_frozen_lockfile(&ctx);
1487 let mut opts = args.into_options(global_frozen, yaml_prefer_frozen, cli_flags, env);
1488 opts.workspace_filter = filter;
1489 commands::install::run(opts).await?;
1490 Ok(())
1491}
1492
1493#[cfg(test)]
1494mod cli_spec_tests {
1495 use super::*;
1496
1497 #[test]
1498 fn every_aubr_run_uses_current_thread_runtime() {
1499 let aubr = Cli::try_parse_test_from(["aubr", "--no-install", "build"])
1500 .expect("aubr --no-install should parse");
1501 assert!(aubr_uses_current_thread(true, &aubr));
1502
1503 let installing =
1504 Cli::try_parse_test_from(["aubr", "build"]).expect("aubr script should parse");
1505 assert!(aubr_uses_current_thread(true, &installing));
1506 assert!(!aubr_uses_current_thread(false, &aubr));
1507 }
1508
1509 #[test]
1510 fn install_accepts_subcommand_registry_flag() {
1511 let cli = Cli::try_parse_test_from([
1512 "aube",
1513 "install",
1514 "--registry",
1515 "https://registry.example.com/",
1516 ])
1517 .expect("install --registry should parse");
1518
1519 let Some(Commands::Install(install_args)) = cli.command else {
1520 panic!("expected install subcommand");
1521 };
1522 assert_eq!(
1523 install_args.network.registry.as_deref(),
1524 Some("https://registry.example.com/")
1525 );
1526 }
1527
1528 #[test]
1529 fn top_level_version_keeps_the_manual_async_path() {
1530 let cli = Cli::try_parse_test_from(["aube", "--version"])
1531 .expect("the manual version flag should bind instead of exiting in the parser");
1532 assert!(cli.version);
1533 }
1534
1535 #[test]
1536 fn install_rejects_incompatible_lockfile_modes() {
1537 assert!(
1538 Cli::try_parse_test_from(["aube", "install", "--fix-lockfile", "--frozen-lockfile"])
1539 .is_err(),
1540 "cross-flatten relationships should be enforced by usage"
1541 );
1542 }
1543
1544 #[test]
1545 fn virtual_store_overrides_conflict() {
1546 for enable in ["--enable-global-virtual-store", "--enable-gvs"] {
1547 for disable in ["--disable-global-virtual-store", "--disable-gvs"] {
1548 assert!(
1549 Cli::try_parse_test_from(["aube", "install", enable, disable]).is_err(),
1550 "{enable} and {disable} should remain mutually exclusive"
1551 );
1552 }
1553 }
1554 }
1555
1556 #[test]
1557 fn pre_subcommand_registry_lifts_to_install() {
1558 let argv = lift_per_subcommand_flags(
1562 [
1563 "aube",
1564 "--registry",
1565 "https://registry.example.com/",
1566 "install",
1567 ]
1568 .into_iter()
1569 .map(OsString::from)
1570 .collect(),
1571 );
1572 let cli = Cli::try_parse_test_from(argv)
1573 .expect("pre-subcommand --registry should still parse via the rewriter");
1574 let Some(Commands::Install(install_args)) = cli.command else {
1575 panic!("expected install subcommand");
1576 };
1577 assert_eq!(
1578 install_args.network.registry.as_deref(),
1579 Some("https://registry.example.com/")
1580 );
1581 }
1582
1583 #[test]
1584 fn dlx_accepts_allow_build_before_command() {
1585 let cli =
1586 Cli::try_parse_test_from(["aube", "dlx", "--allow-build=esbuild", "vite", "--version"])
1587 .expect("dlx --allow-build should parse");
1588
1589 let Some(Commands::Dlx(dlx_args)) = cli.command else {
1590 panic!("expected dlx subcommand");
1591 };
1592 assert_eq!(dlx_args.allow_build, ["esbuild"]);
1593 assert_eq!(dlx_args.params, ["vite", "--version"]);
1594 }
1595
1596 #[test]
1597 fn dlx_rejects_empty_allow_build_value() {
1598 let err = match Cli::try_parse_test_from(["aube", "dlx", "--allow-build=", "vite"]) {
1599 Ok(_) => panic!("empty --allow-build should fail"),
1600 Err(err) => err,
1601 };
1602 assert!(
1603 err.to_string()
1604 .contains("The --allow-build flag is missing a package name"),
1605 "{err}"
1606 );
1607 }
1608
1609 #[test]
1610 fn add_accepts_dangerously_allow_all_builds_after_package() {
1611 let cli = Cli::try_parse_test_from([
1612 "aube",
1613 "add",
1614 "--global",
1615 "opencode-ai@1.17.13",
1616 "--dangerously-allow-all-builds",
1617 ])
1618 .expect("add --dangerously-allow-all-builds should parse after a package");
1619
1620 let Some(Commands::Add(add_args)) = cli.command else {
1621 panic!("expected add subcommand");
1622 };
1623 assert!(add_args.global);
1624 assert!(add_args.dangerously_allow_all_builds);
1625 assert_eq!(add_args.packages, ["opencode-ai@1.17.13"]);
1626 }
1627
1628 #[test]
1629 fn add_rejects_deny_build_with_dangerously_allow_all_builds() {
1630 let err = match Cli::try_parse_test_from([
1631 "aube",
1632 "add",
1633 "some-package",
1634 "--deny-build=some-package",
1635 "--dangerously-allow-all-builds",
1636 ]) {
1637 Ok(_) => panic!("deny-build should conflict with dangerously-allow-all-builds"),
1638 Err(err) => err,
1639 };
1640
1641 assert!(
1642 err.to_string()
1643 .contains("'--deny-build' cannot be used with '--dangerously-allow-all-builds'"),
1644 "{err}"
1645 );
1646 }
1647
1648 #[test]
1649 fn lifter_does_not_eat_lifted_flag_as_kept_flag_value() {
1650 let argv = lift_per_subcommand_flags(
1655 ["aube", "--dir", "--frozen-lockfile", "install"]
1656 .into_iter()
1657 .map(OsString::from)
1658 .collect(),
1659 );
1660 let strs: Vec<&str> = argv.iter().filter_map(|t| t.to_str()).collect();
1663 let install_idx = strs
1664 .iter()
1665 .position(|s| *s == "install")
1666 .expect("install subcommand should survive the lift");
1667 assert!(
1668 strs[install_idx + 1..].contains(&"--frozen-lockfile"),
1669 "--frozen-lockfile should land after the subcommand: {strs:?}"
1670 );
1671 }
1672
1673 #[test]
1674 fn short_command_aliases_parse() {
1675 let cli = Cli::try_parse_test_from(["aube", "a", "react"]).expect("a should parse as add");
1676 assert!(matches!(cli.command, Some(Commands::Add(_))));
1677
1678 let cli = Cli::try_parse_test_from(["aube", "x", "vitest", "--run"])
1679 .expect("x should parse as exec");
1680 let Some(Commands::Exec(args)) = cli.command else {
1681 panic!("x should dispatch to exec");
1682 };
1683 assert_eq!(args.bin, "vitest");
1684 assert_eq!(args.args, vec!["--run"]);
1685
1686 let cli = Cli::try_parse_test_from(["aube", "w", "react"]).expect("w should parse as why");
1687 assert!(matches!(cli.command, Some(Commands::Why(_))));
1688 }
1689
1690 #[test]
1691 fn bin_accepts_workspace_root_flag() {
1692 for flag in ["-w", "--workspace-root", "--workspace"] {
1695 let cli = Cli::try_parse_test_from(["aube", "bin", flag])
1696 .unwrap_or_else(|e| panic!("`aube bin {flag}` should parse: {e}"));
1697 let Some(Commands::Bin(args)) = cli.command else {
1698 panic!("`aube bin {flag}` should dispatch to bin");
1699 };
1700 assert!(args.workspace_root, "{flag} should set workspace_root");
1701 assert!(!args.global);
1702 }
1703 }
1704
1705 #[test]
1706 fn bin_global_conflicts_with_workspace_root() {
1707 assert!(
1708 Cli::try_parse_test_from(["aube", "bin", "-g", "-w"]).is_err(),
1709 "`aube bin -g -w` should be rejected as conflicting"
1710 );
1711 }
1712
1713 #[test]
1714 fn node_subcommand_forwards_version_flag() {
1715 let cli = Cli::try_parse_test_from(rewrite_multicall_argv(vec![
1716 OsString::from("aube"),
1717 OsString::from("node"),
1718 OsString::from("--version"),
1719 ]))
1720 .expect("node --version parses");
1721 assert!(!should_print_top_level_version(&cli));
1722 let Some(Commands::Node(args)) = cli.command else {
1723 panic!("expected node subcommand");
1724 };
1725 assert_eq!(args.args, vec![OsString::from("--version")]);
1726 }
1727
1728 #[test]
1729 fn node_subcommand_forwards_help_flag() {
1730 let cli = Cli::try_parse_test_from(rewrite_multicall_argv(vec![
1731 OsString::from("aube"),
1732 OsString::from("node"),
1733 OsString::from("--help"),
1734 ]))
1735 .expect("node --help parses");
1736 let Some(Commands::Node(args)) = cli.command else {
1737 panic!("expected node subcommand");
1738 };
1739 assert_eq!(args.args, vec![OsString::from("--help")]);
1740 }
1741}
1742
1743#[cfg(test)]
1744mod multicall_tests {
1745 use super::*;
1746
1747 fn os(strs: &[&str]) -> Vec<OsString> {
1748 strs.iter().map(OsString::from).collect()
1749 }
1750
1751 fn temp_shim(name: &str) -> tempfile::TempDir {
1752 let dir = tempfile::tempdir().expect("temp dir should be created");
1753 std::fs::write(dir.path().join(name), "#!/tmp/aube.exe\n").expect("shim should be written");
1754 dir
1755 }
1756
1757 #[test]
1758 fn aube_passes_through_unchanged() {
1759 assert_eq!(
1760 rewrite_multicall_argv(os(&["aube", "install"])),
1761 os(&["aube", "install"])
1762 );
1763 }
1764
1765 #[test]
1766 fn aubr_is_left_for_the_executable_view() {
1767 assert_eq!(
1768 rewrite_multicall_argv(os(&["aubr", "build"])),
1769 os(&["aubr", "build"])
1770 );
1771 }
1772
1773 #[test]
1774 fn aubx_is_left_for_the_executable_view() {
1775 assert_eq!(
1776 rewrite_multicall_argv(os(&["aubx", "cowsay", "hi"])),
1777 os(&["aubx", "cowsay", "hi"])
1778 );
1779 }
1780
1781 #[test]
1782 fn executable_views_dispatch_without_argv_rewriting() {
1783 let cli = Cli::try_parse_test_from(["aubr", "build"])
1784 .expect("aubr should promote the run command");
1785 let Some(Commands::Run(run)) = cli.command else {
1786 panic!("expected run");
1787 };
1788 assert_eq!(run.script.as_deref(), Some("build"));
1789
1790 let raw = os(&["aubx", "cowsay"]);
1791 let argv: Vec<&std::ffi::OsStr> = raw.iter().map(OsString::as_os_str).collect();
1792 let cli = Cli::parse_from_argv(&argv).expect("aubx should promote the dlx command");
1793 assert!(matches!(cli.command, Some(Commands::Dlx(_))));
1794
1795 let raw = os(&["aubr", "--version"]);
1796 let argv: Vec<&std::ffi::OsStr> = raw.iter().map(OsString::as_os_str).collect();
1797 let cli = Cli::parse_from_argv(&argv).expect("the global manual version flag should bind");
1798 assert!(cli.version);
1799 assert!(matches!(cli.command, Some(Commands::Run(_))));
1800 }
1801
1802 #[test]
1803 fn executable_view_flags_are_not_lifted_past_forwarded_positionals() {
1804 let argv = os(&["aubr", "--registry", "https://registry.test", "build"]);
1805 assert_eq!(lift_per_subcommand_flags(argv.clone()), argv);
1806
1807 let argv = os(&["aubx", "--registry", "https://registry.test", "vite"]);
1808 assert_eq!(lift_per_subcommand_flags(argv.clone()), argv);
1809 }
1810
1811 #[test]
1812 fn node_shim_rewrites_to_node_subcommand() {
1813 assert_eq!(
1814 rewrite_multicall_argv(os(&["node", "--version"])),
1815 os(&["aube", "node", "--", "--version"])
1816 );
1817 assert_eq!(
1818 rewrite_multicall_argv(os(&["aube", "node", "--version"])),
1819 os(&["aube", "node", "--", "--version"])
1820 );
1821 assert_eq!(
1822 rewrite_multicall_argv(os(&["aube", "__aube-shim", "node", "--version"])),
1823 os(&["aube", "node", "--", "--version"])
1824 );
1825 }
1826
1827 #[test]
1828 fn package_manager_version_flags_rewrite_to_aube_version() {
1829 assert_eq!(
1830 rewrite_multicall_argv(os(&["pnpm", "--version"])),
1831 os(&["aube", "--version"])
1832 );
1833 assert_eq!(
1834 rewrite_multicall_argv(os(&["npm", "-v"])),
1835 os(&["aube", "--version"])
1836 );
1837 assert_eq!(
1838 rewrite_multicall_argv(os(&["npx", "-v"])),
1839 os(&["aube", "--version"])
1840 );
1841 assert_eq!(
1842 rewrite_multicall_argv(os(&["pnpx", "--version"])),
1843 os(&["aube", "--version"])
1844 );
1845 }
1846
1847 #[test]
1848 fn pnpm_and_pnpx_shims_rewrite_to_aube_surfaces() {
1849 assert_eq!(
1850 rewrite_multicall_argv(os(&["pnpm", "install", "--frozen-lockfile"])),
1851 os(&["aube", "install", "--frozen-lockfile"])
1852 );
1853 assert_eq!(
1854 rewrite_multicall_argv(os(&["pnpx", "cowsay", "hi"])),
1855 os(&["aube", "dlx", "cowsay", "hi"])
1856 );
1857 assert_eq!(
1858 rewrite_multicall_argv(os(&[
1859 "aube",
1860 "__aube-shim",
1861 "pnpm",
1862 "install",
1863 "--frozen-lockfile",
1864 ])),
1865 os(&["aube", "install", "--frozen-lockfile"])
1866 );
1867 }
1868
1869 #[test]
1870 fn dispatcher_marker_rejects_unknown_tool_names() {
1871 assert_eq!(
1872 rewrite_multicall_argv(os(&["aube", "__aube-shim", "shell", "arg"])),
1873 os(&["aube", "__aube-shim", "shell", "arg"])
1874 );
1875 }
1876
1877 #[test]
1878 fn npm_install_rewrites_to_install_or_add() {
1879 assert_eq!(
1880 rewrite_multicall_argv(os(&["npm", "install"])),
1881 os(&["aube", "install"])
1882 );
1883 assert_eq!(
1884 rewrite_multicall_argv(os(&["npm", "i", "-D", "vitest"])),
1885 os(&["aube", "add", "-D", "vitest"])
1886 );
1887 assert_eq!(
1888 rewrite_multicall_argv(os(&["npm", "install", "--registry", "https://r.test"])),
1889 os(&["aube", "install", "--registry", "https://r.test"])
1890 );
1891 assert_eq!(
1892 rewrite_multicall_argv(os(&["npm", "install", "--workspace", "app"])),
1893 os(&["aube", "install", "--workspace", "app"])
1894 );
1895 assert_eq!(
1896 rewrite_multicall_argv(os(&["npm", "install", "--"])),
1897 os(&["aube", "install", "--"])
1898 );
1899 assert_eq!(
1900 rewrite_multicall_argv(os(&["npm", "install", "--", "vitest"])),
1901 os(&["aube", "add", "--", "vitest"])
1902 );
1903 }
1904
1905 #[test]
1906 fn npm_common_commands_rewrite_to_aube() {
1907 assert_eq!(
1908 rewrite_multicall_argv(os(&["npm", "ci"])),
1909 os(&["aube", "ci"])
1910 );
1911 assert_eq!(
1912 rewrite_multicall_argv(os(&["npm", "run", "build"])),
1913 os(&["aube", "run", "build"])
1914 );
1915 assert_eq!(
1916 rewrite_multicall_argv(os(&["npm", "rm", "react"])),
1917 os(&["aube", "remove", "react"])
1918 );
1919 assert_eq!(
1920 rewrite_multicall_argv(os(&["npx", "vite", "--version"])),
1921 os(&["aube", "dlx", "vite", "--version"])
1922 );
1923 }
1924
1925 #[test]
1926 fn yarn_common_commands_rewrite_to_aube() {
1927 assert_eq!(
1928 rewrite_multicall_argv(os(&["yarn"])),
1929 os(&["aube", "install"])
1930 );
1931 assert_eq!(
1932 rewrite_multicall_argv(os(&["yarn", "add", "react"])),
1933 os(&["aube", "add", "react"])
1934 );
1935 assert_eq!(
1936 rewrite_multicall_argv(os(&["yarnpkg", "remove", "react"])),
1937 os(&["aube", "remove", "react"])
1938 );
1939 }
1940
1941 #[test]
1942 fn absolute_path_and_exe_suffix_are_handled() {
1943 assert_eq!(
1945 rewrite_multicall_argv(os(&["/usr/local/bin/aubr", "test"])),
1946 os(&["/usr/local/bin/aubr", "test"])
1947 );
1948 assert_eq!(
1949 rewrite_multicall_argv(os(&["aubx.exe", "pkg"])),
1950 os(&["aubx.exe", "pkg"])
1951 );
1952 }
1953
1954 #[test]
1955 fn bare_shim_invocation_passes_through_to_subcommand() {
1956 assert_eq!(rewrite_multicall_argv(os(&["aubr"])), os(&["aubr"]));
1957 }
1958
1959 #[test]
1960 fn version_flag_short_circuits_to_top_level() {
1961 assert_eq!(
1963 rewrite_multicall_argv(os(&["aubr", "--version"])),
1964 os(&["aubr", "--version"])
1965 );
1966 assert_eq!(
1967 rewrite_multicall_argv(os(&["aubx", "--version"])),
1968 os(&["aubx", "--version"])
1969 );
1970 assert_eq!(
1971 rewrite_multicall_argv(os(&["aubr", "-V"])),
1972 os(&["aubr", "-V"])
1973 );
1974 assert_eq!(
1975 rewrite_multicall_argv(os(&["aubx.exe", "-V"])),
1976 os(&["aubx.exe", "-V"])
1977 );
1978 }
1979
1980 #[test]
1981 fn npm_interpreter_shim_path_is_dropped() {
1982 let dir = temp_shim("aube");
1983 let shim = dir.path().join("aube");
1984 let shim_os = shim.clone().into_os_string();
1985 assert_eq!(
1986 rewrite_multicall_argv(vec![
1987 OsString::from("aube.exe"),
1988 shim.into_os_string(),
1989 OsString::from("--version"),
1990 ]),
1991 vec![shim_os, OsString::from("--version")]
1992 );
1993 }
1994
1995 #[test]
1996 fn npm_interpreter_shim_preserves_multicall_dispatch() {
1997 let dir = temp_shim("aubr");
1998 let shim = dir.path().join("aubr");
1999 assert_eq!(
2000 rewrite_multicall_argv(vec![
2001 OsString::from("aubr.exe"),
2002 shim.clone().into_os_string(),
2003 OsString::from("build"),
2004 ]),
2005 vec![shim.into_os_string(), OsString::from("build")]
2006 );
2007 }
2008
2009 #[test]
2010 fn extract_config_overrides_strips_equals_form() {
2011 let mut argv = os(&["aube", "install", "--config.strict-dep-builds=true"]);
2012 let parsed = extract_config_overrides(&mut argv);
2013 assert_eq!(argv, os(&["aube", "install"]));
2014 assert_eq!(
2015 parsed,
2016 vec![("strict-dep-builds".to_string(), "true".to_string())]
2017 );
2018 }
2019
2020 #[test]
2021 fn extract_config_overrides_strips_bool_form() {
2022 let mut argv = os(&["aube", "--config.strictDepBuilds", "install"]);
2023 let parsed = extract_config_overrides(&mut argv);
2024 assert_eq!(argv, os(&["aube", "install"]));
2025 assert_eq!(
2026 parsed,
2027 vec![("strictDepBuilds".to_string(), "true".to_string())]
2028 );
2029 }
2030
2031 #[test]
2032 fn extract_config_overrides_handles_multiple_and_preserves_order() {
2033 let mut argv = os(&[
2034 "aube",
2035 "--config.foo=1",
2036 "install",
2037 "--config.bar=two",
2038 "--config.foo=3",
2039 ]);
2040 let parsed = extract_config_overrides(&mut argv);
2041 assert_eq!(argv, os(&["aube", "install"]));
2042 assert_eq!(
2043 parsed,
2044 vec![
2045 ("foo".to_string(), "1".to_string()),
2046 ("bar".to_string(), "two".to_string()),
2047 ("foo".to_string(), "3".to_string()),
2048 ]
2049 );
2050 }
2051
2052 #[test]
2053 fn extract_config_overrides_stops_at_double_dash() {
2054 let mut argv = os(&["aube", "exec", "--", "node", "--config.foo=should-stay"]);
2055 let parsed = extract_config_overrides(&mut argv);
2056 assert!(parsed.is_empty());
2057 assert_eq!(
2058 argv,
2059 os(&["aube", "exec", "--", "node", "--config.foo=should-stay"])
2060 );
2061 }
2062
2063 #[test]
2064 fn extract_config_overrides_preserves_argv_when_absent() {
2065 let mut argv = os(&["aube", "install", "--frozen-lockfile"]);
2066 let parsed = extract_config_overrides(&mut argv);
2067 assert!(parsed.is_empty());
2068 assert_eq!(argv, os(&["aube", "install", "--frozen-lockfile"]));
2069 }
2070}
2071
2072#[cfg(test)]
2073mod package_manager_guard_tests {
2074 use super::*;
2075
2076 #[test]
2077 fn run_like_commands_warn_instead_of_erroring() {
2078 let run = Cli::try_parse_test_from(["aube", "run", "test"]).expect("run should parse");
2079 let test = Cli::try_parse_test_from(["aube", "test"]).expect("test should parse");
2080
2081 assert_eq!(
2082 package_manager_guard_mode(run.command.as_ref()),
2083 PackageManagerGuardMode::WarnAndSkipAutoInstall
2084 );
2085 assert_eq!(
2086 package_manager_guard_mode(test.command.as_ref()),
2087 PackageManagerGuardMode::WarnAndSkipAutoInstall
2088 );
2089 }
2090
2091 #[test]
2092 fn install_still_errors_on_mismatch() {
2093 let cli = Cli::try_parse_test_from(["aube", "install"]).expect("install should parse");
2094 assert_eq!(
2095 package_manager_guard_mode(cli.command.as_ref()),
2096 PackageManagerGuardMode::Error
2097 );
2098 }
2099
2100 #[test]
2101 fn install_test_still_errors_on_mismatch() {
2102 let cli =
2103 Cli::try_parse_test_from(["aube", "install-test"]).expect("install-test should parse");
2104 assert_eq!(
2105 package_manager_guard_mode(cli.command.as_ref()),
2106 PackageManagerGuardMode::Error
2107 );
2108 }
2109
2110 #[test]
2111 fn prefix_skips_package_manager_guard() {
2112 let cli = Cli::try_parse_test_from(["aube", "prefix"]).expect("prefix should parse");
2113 assert!(!command_needs_package_manager_guard(cli.command.as_ref()));
2114 }
2115
2116 #[test]
2117 fn package_manager_strict_mode_parses_canonical_spellings() {
2118 for (input, expected) in [
2119 ("off", PackageManagerStrictMode::Off),
2120 ("warn", PackageManagerStrictMode::Warn),
2121 ("error", PackageManagerStrictMode::Error),
2122 (" ERROR\n", PackageManagerStrictMode::Error),
2123 ] {
2124 assert_eq!(PackageManagerStrictMode::parse(input), Some(expected));
2125 }
2126 }
2127
2128 #[test]
2129 fn package_manager_strict_mode_parses_bool_back_compat() {
2130 for (input, expected) in [
2134 ("true", PackageManagerStrictMode::Error),
2135 ("false", PackageManagerStrictMode::Off),
2136 ("1", PackageManagerStrictMode::Error),
2137 ("0", PackageManagerStrictMode::Off),
2138 ] {
2139 assert_eq!(PackageManagerStrictMode::parse(input), Some(expected));
2140 }
2141 }
2142
2143 #[test]
2144 fn package_manager_strict_mode_returns_none_for_typos() {
2145 assert!(PackageManagerStrictMode::parse("errror").is_none());
2149 assert!(PackageManagerStrictMode::parse("warning").is_none());
2150 assert!(PackageManagerStrictMode::parse("").is_none());
2151 }
2152}
2153
2154#[cfg(test)]
2155mod cli_ordering_tests {
2156 use super::*;
2157 use std::collections::BTreeMap;
2158
2159 #[test]
2173 fn test_cli_ordering() {
2174 check_command_sorted(Cli::spec().root, &[]);
2175 }
2176
2177 fn check_command_sorted(cmd: &usage_rs::spec::CommandMeta<'_>, path: &[&str]) {
2178 let mut current_path: Vec<&str> = path.to_vec();
2179 current_path.push(cmd.cmd.name);
2180
2181 let names: Vec<_> = cmd.subcommands.iter().map(|sub| sub.cmd.name).collect();
2183 let mut sorted = names.clone();
2184 sorted.sort();
2185 assert!(
2186 names == sorted,
2187 "Subcommands in '{}' are not sorted alphabetically!\nActual: {:?}\nExpected: {:?}",
2188 current_path.join(" "),
2189 names,
2190 sorted,
2191 );
2192
2193 let mut shorts: Vec<u8> = Vec::new();
2195 let mut by_heading: BTreeMap<Option<&str>, Vec<&str>> = BTreeMap::new();
2196 for flag in cmd.flags {
2197 if let Some(&short) = flag.flag.shorts.first() {
2198 shorts.push(short);
2199 } else if let Some(&long) = flag.flag.longs.first() {
2200 by_heading.entry(flag.help_heading).or_default().push(long);
2201 }
2202 }
2203 let mut sorted_shorts = shorts.clone();
2204 sorted_shorts.sort_by_key(|c| (c.to_ascii_lowercase(), c.is_ascii_uppercase()));
2205 assert!(
2206 shorts == sorted_shorts,
2207 "Short flags in '{}' are not sorted!\nActual: {:?}\nExpected: {:?}",
2208 current_path.join(" "),
2209 shorts,
2210 sorted_shorts,
2211 );
2212 for (heading, longs) in &by_heading {
2213 let mut sorted_longs = longs.clone();
2214 sorted_longs.sort();
2215 assert!(
2216 longs == &sorted_longs,
2217 "Long-only flags under heading {:?} in '{}' are not sorted!\nActual: {:?}\nExpected: {:?}",
2218 heading,
2219 current_path.join(" "),
2220 longs,
2221 sorted_longs,
2222 );
2223 }
2224 for sub in cmd.subcommands {
2225 check_command_sorted(sub, ¤t_path);
2226 }
2227 }
2228}