Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 85 additions & 38 deletions crates/nub-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,12 @@ pub enum Command {
#[arg(long = "no-check")]
no_check: bool,

/// Per-invocation `minimumReleaseAge` overrides for the fetch path.
/// `nubx <just-published-tool>` is the common way to hit the age gate,
/// so the escape hatch belongs on this surface.
#[command(flatten)]
age_gate: crate::pm_engine::AgeGateFlags,

/// Remaining arguments forwarded to the binary.
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
Expand Down Expand Up @@ -1108,6 +1114,9 @@ pub enum Command {

#[command(flatten)]
output: crate::pm_engine::OutputFlags,

#[command(flatten)]
age_gate: crate::pm_engine::AgeGateFlags,
},

/// Clean install for CI: delete node_modules, install strictly from the
Expand Down Expand Up @@ -1152,6 +1161,9 @@ pub enum Command {

#[command(flatten)]
output: crate::pm_engine::OutputFlags,

#[command(flatten)]
age_gate: crate::pm_engine::AgeGateFlags,
},
}

Expand Down Expand Up @@ -1379,6 +1391,10 @@ fn install_to_add_args(rest: &[String]) -> Option<Vec<String>> {
// them here prevents the space-separated value from looking like a pkg.
"--loglevel",
"--reporter",
// Same shape: `nub install --minimum-release-age 0` would otherwise
// read `0` as a package and route the whole command to `add`.
"--minimum-release-age",
"--minimum-release-age-exclude",
];
let mut i = 0;
while i < body.len() {
Expand Down Expand Up @@ -2108,6 +2124,10 @@ fn value_consuming_flags(subcommand: &str) -> &'static [&'static str] {
// (repeatable, takes the package spec as a following token). They must be
// listed so `nubx -p left-pad cowsay` binds `left-pad` to the package, not
// the bin positional.
// The age-gate value flags take a following token, so `nubx
// --minimum-release-age 1d cowsay` must bind `1d` to the flag rather
// than read it as the bin. Both age-gate flags are value-taking; there
// is no boolean sibling, since nub ships no strictness flag.
"nubx" => &[
"--filter",
"-F",
Expand All @@ -2116,6 +2136,8 @@ fn value_consuming_flags(subcommand: &str) -> &'static [&'static str] {
"--package",
"-p",
"--cwd",
"--minimum-release-age",
"--minimum-release-age-exclude",
],
"watch" => &["--cwd"],
_ => &[],
Expand Down Expand Up @@ -2491,12 +2513,17 @@ fn dispatch_subcommand(rest: Vec<String>) -> Result<i32> {
yes,
ignore_existing,
no_check,
age_gate,
mut args,
}) => {
args.extend(suffix);
if no_check {
crate::verify_deps::disable();
}
// Only the DLX fallback below resolves from the registry, but the
// bag is inert on the local-bin path, so publish unconditionally
// rather than duplicating the call into each branch.
age_gate.apply();
filter.extend(workspace);
let recursive = recursive || parallel || include_workspace_root;
let workspace_run = recursive || !filter.is_empty() || parallel;
Expand Down Expand Up @@ -2610,30 +2637,34 @@ fn dispatch_subcommand(rest: Vec<String>) -> Result<i32> {
fail_if_no_match,
include_workspace_root,
output,
}) => crate::pm_engine::run_install(crate::pm_engine::InstallFlags {
frozen_lockfile,
no_frozen_lockfile,
prefer_frozen_lockfile,
prod,
dev,
ignore_scripts,
no_optional,
offline,
prefer_offline,
lockfile_only,
force,
node_linker,
registry,
dir,
filter: crate::pm_engine::WorkspaceFilterFlags {
filter,
filter_prod,
recursive,
fail_if_no_match,
include_workspace_root,
},
output,
}),
age_gate,
}) => {
age_gate.apply();
crate::pm_engine::run_install(crate::pm_engine::InstallFlags {
frozen_lockfile,
no_frozen_lockfile,
prefer_frozen_lockfile,
prod,
dev,
ignore_scripts,
no_optional,
offline,
prefer_offline,
lockfile_only,
force,
node_linker,
registry,
dir,
filter: crate::pm_engine::WorkspaceFilterFlags {
filter,
filter_prod,
recursive,
fail_if_no_match,
include_workspace_root,
},
output,
})
}
Some(Command::Ci {
ignore_scripts,
no_optional,
Expand All @@ -2645,20 +2676,24 @@ fn dispatch_subcommand(rest: Vec<String>) -> Result<i32> {
fail_if_no_match,
include_workspace_root,
output,
}) => crate::pm_engine::run_ci(crate::pm_engine::CiFlags {
ignore_scripts,
no_optional,
registry,
dir,
filter: crate::pm_engine::WorkspaceFilterFlags {
filter,
filter_prod,
recursive,
fail_if_no_match,
include_workspace_root,
},
output,
}),
age_gate,
}) => {
age_gate.apply();
crate::pm_engine::run_ci(crate::pm_engine::CiFlags {
ignore_scripts,
no_optional,
registry,
dir,
filter: crate::pm_engine::WorkspaceFilterFlags {
filter,
filter_prod,
recursive,
fail_if_no_match,
include_workspace_root,
},
output,
})
}
// `node` is intercepted at the top of `dispatch_subcommand` (manual
// sub-verb match in `run_node`) and never reaches clap here.
Some(Command::Node { .. }) => unreachable!("`node` is handled before clap dispatch"),
Expand Down Expand Up @@ -10185,6 +10220,18 @@ mod tests {
None,
"nub install --loglevel=silent stays on the native install path (equals form)"
);
// `--minimum-release-age` has the same shape: its MINUTES value in the
// space form would read as a package spec and misroute the install.
assert_eq!(
install_to_add_args(&args(&["install", "--minimum-release-age", "0"])),
None,
"nub install --minimum-release-age 0 stays on the native install path"
);
assert_eq!(
install_to_add_args(&args(&["install", "--minimum-release-age", "0", "react"])),
Some(args(&["add", "--minimum-release-age", "0", "react"])),
"--minimum-release-age 0 with a package routes to add, minutes not mis-forwarded"
);
// Output-control flags combined with a real package still route to add,
// with the flag consumed as a flag (value NOT forwarded as a package).
assert_eq!(
Expand Down
51 changes: 44 additions & 7 deletions crates/nub-cli/src/pm_engine/install_family.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,19 @@ pub(crate) fn run_verb(

// ───────────────────────── parse plumbing ──────────────────────────

// The subset of aube's *global* clap flags nub honors on engine verbs,
// parsed at the verb position (nub has no pre-verb engine flag surface).
// Spellings mirror `vendor/aube/crates/aube/src/lib.rs::Cli` exactly.
// What nub augments onto every install-family verb, parsed at the verb
// position (nub has no pre-verb engine flag surface). TWO groups, and the
// distinction matters when adding a field:
//
// 1. The subset of aube's *global* clap flags nub honors. These mirror
// `vendor/aube/crates/aube/src/lib.rs::Cli` exactly — `--dir`, the
// workspace selectors, and the output flags below.
// 2. NUB-OWN flags with no aube `Cli` counterpart: the `age_gate` group.
// aube exposes `minimumReleaseAge` only through config and its generic
// `--config.<key>` extractor (which is dead under nub, living in aube's
// unused `cli_main`), so these spellings come from pnpm's CLI type map
// rather than from aube.
//
// Deliberately absent: `--workspace-root` (aube chdirs to the workspace
// root pre-dispatch; the helper is crate-private, and half-honoring the
// flag as filter-only would silently run against the wrong directory —
Expand Down Expand Up @@ -224,6 +234,27 @@ struct EngineGlobals {
/// forwarded to the engine's text-mode renderers.
#[command(flatten)]
output: super::output::OutputFlags,

/// Per-invocation `minimumReleaseAge` overrides — a NUB-OWN group, not an
/// aube `Cli` mirror (see the note above the struct).
///
/// This rides on `EngineGlobals`, so it reaches EVERY verb in the family —
/// all ~20 `run_verb` arms, verified by sweeping `--help`, not estimated.
/// Only a minority actually consult it: `add`/`update`/`dedupe`/`import`
/// resolve from the registry directly, and `dlx`/`create` through their
/// transient install. The rest accept it and ignore it — `remove` and
/// `prune` only re-resolve or trim what is already there, and the
/// local-state verbs (`link`/`unlink`, `approve-builds`/`ignored-builds`,
/// the `patch*` trio, `rebuild`) never resolve at all.
///
/// Carried anyway rather than split per-verb: the cost is an inert `--help`
/// line on those verbs, and threading a second args group through the
/// family would be more machinery than that is worth. The read-only verbs
/// (`why`, `list`, `licenses`, `outdated`, `audit`) are unaffected either
/// way — they parse through `info_family`'s own `parse_verb` and never see
/// this struct.
#[command(flatten)]
age_gate: super::min_release_age::AgeGateFlags,
}

impl EngineGlobals {
Expand Down Expand Up @@ -267,10 +298,14 @@ fn verb_command<A: ClapArgs>(typed: &str) -> clap::Command {
fn parse_verb<A: ClapArgs>(typed: &str, args: &[String]) -> Result<ParsedVerb<A>> {
let argv = std::iter::once(format!("nub {typed}")).chain(args.iter().cloned());
match verb_command::<A>(typed).try_get_matches_from(argv) {
Ok(matches) => Ok(ParsedVerb::Run(
EngineGlobals::from_arg_matches(&matches)?,
A::from_arg_matches(&matches)?,
)),
Ok(matches) => {
let globals = EngineGlobals::from_arg_matches(&matches)?;
// Publish before the verb runs: the engine reads the age gate
// through process-global settings accessors, not through anything
// threaded into the verb's own args.
globals.age_gate.apply();
Ok(ParsedVerb::Run(globals, A::from_arg_matches(&matches)?))
}
Err(err) => {
let text = present::rewrite_help(err.render().to_string().trim_end());
if matches!(
Expand Down Expand Up @@ -1719,6 +1754,8 @@ mod tests {
"--filter",
"--filter-prod",
"--loglevel",
"--minimum-release-age",
"--minimum-release-age-exclude",
"--package",
"--prefix",
"--registry",
Expand Down
Loading
Loading