Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "global-option-conflicts",
"private": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[[case]]
name = "managed_global_rejects_project_options"
vp = "global"
comment = "Project-only flags are rejected before managed global operations can install, remove, or update packages."
steps = [
{ argv = ["vp", "install", "-g", "-D", "--frozen-lockfile", "--filter", "app", "typescript"], continue-on-failure = true },
{ argv = ["vp", "add", "-g", "--allow-build", "typescript", "typescript"], continue-on-failure = true },
{ argv = ["vp", "remove", "-g", "--filter", "app", "typescript"], continue-on-failure = true },
{ argv = ["vp", "update", "-g", "--interactive", "typescript"], continue-on-failure = true },
{ argv = ["vp", "outdated", "-g", "--compatible"], continue-on-failure = true },
{ argv = ["vp", "pm", "list", "-g", "--depth", "0"], continue-on-failure = true },
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# managed_global_rejects_project_options

Project-only flags are rejected before managed global operations can install, remove, or update packages.

## `vp install -g -D --frozen-lockfile --filter app typescript`

**Exit code:** 2

```
error: the argument '--global' cannot be used with:
--dev
--frozen-lockfile
--filter <PATTERN>

Usage: vp install --global <PACKAGES>... [-- <PASS_THROUGH_ARGS>...]

For more information, try '--help'.
```

## `vp add -g --allow-build typescript typescript`

**Exit code:** 2

```
error: the argument '--global' cannot be used with '--allow-build <NAMES>'

Usage: vp add --global <PACKAGES>... [-- <PASS_THROUGH_ARGS>...]

For more information, try '--help'.
```

## `vp remove -g --filter app typescript`

**Exit code:** 2

```
error: the argument '--global' cannot be used with '--filter <PATTERN>'

Usage: vp remove --global <PACKAGES>... [-- <PASS_THROUGH_ARGS>...]

For more information, try '--help'.
```

## `vp update -g --interactive typescript`

**Exit code:** 2

```
error: the argument '--global' cannot be used with '--interactive'

Usage: vp update --global <PACKAGES>... [-- <PASS_THROUGH_ARGS>...]

For more information, try '--help'.
```

## `vp outdated -g --compatible`

**Exit code:** 2

```
error: the argument '--global' cannot be used with '--compatible'

Usage: vp outdated --global [PACKAGES]... [-- <PASS_THROUGH_ARGS>...]

For more information, try '--help'.
```

## `vp pm list -g --depth 0`

**Exit code:** 2

```
error: the argument '--global' cannot be used with '--depth <DEPTH>'

Usage: vp pm list --global [PATTERN] [-- <PASS_THROUGH_ARGS>...]

For more information, try '--help'.
```
129 changes: 126 additions & 3 deletions crates/vp_pm_cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -783,11 +783,134 @@ mod tests {
assert!(add.is_managed_global());
}

#[test]
fn managed_global_rejects_project_only_options() {
let cases: &[(&[&str], &[&[&str]])] = &[
(
&["install"],
&[
&["--prod"],
&["--dev"],
&["--no-optional"],
&["--frozen-lockfile"],
&["--no-frozen-lockfile"],
&["--lockfile-only"],
&["--prefer-offline"],
&["--offline"],
&["--no-lockfile"],
&["--fix-lockfile"],
&["--shamefully-hoist"],
&["--resolution-only"],
&["--filter", "app"],
&["--workspace-root"],
&["--save-exact"],
&["--save-peer"],
&["--save-optional"],
&["--save-catalog"],
],
),
(
&["add"],
&[
&["--save-prod"],
&["--save-dev"],
&["--save-peer"],
&["--save-optional"],
&["--save-exact"],
&["--save-catalog-name", "default"],
&["--save-catalog"],
&["--allow-build", "react"],
&["--filter", "app"],
&["--workspace-root"],
&["--workspace"],
],
),
(
&["remove"],
&[
&["--save-dev"],
&["--save-optional"],
&["--save-prod"],
&["--filter", "app"],
&["--workspace-root"],
&["--recursive"],
],
),
(
&["update"],
&[
&["--recursive"],
&["--filter", "app"],
&["--workspace-root"],
&["--dev"],
&["--prod"],
&["--interactive"],
&["--no-optional"],
&["--no-save"],
&["--workspace"],
],
),
(
&["outdated"],
&[
&["--recursive"],
&["--filter", "app"],
&["--workspace-root"],
&["--prod"],
&["--dev"],
&["--no-optional"],
&["--compatible"],
&["--sort-by", "name"],
],
),
(
&["pm", "list"],
&[
&["--depth", "0"],
&["--long"],
&["--parseable"],
&["--prod"],
&["--dev"],
&["--no-optional"],
&["--exclude-peers"],
&["--only-projects"],
&["--find-by", "finder"],
&["--recursive"],
&["--filter", "app"],
],
),
];
for &(command, options) in cases {
for option in options {
let mut argv = command.to_vec();
argv.extend_from_slice(option);
argv.push("react");
assert!(parse(&argv).is_ok(), "{argv:?}");
for global in ["-g", "--global"] {
let mut argv = argv.clone();
argv.push(global);
let error = parse(&argv).unwrap_err();
assert_eq!(error.kind(), clap::error::ErrorKind::ArgumentConflict, "{argv:?}");
assert!(error.to_string().contains("--global"), "{error}");
}
}
}
}

#[test]
fn projects_managed_global_commands() {
let install =
parse(&["install", "-g", "--node", "22", "--force", "--concurrency", "2", "tsx"])
.unwrap();
let install = parse(&[
"install",
"-g",
"--node",
"22",
"--force",
"--silent",
"--concurrency",
"2",
"tsx",
])
.unwrap();
let Some(ManagedGlobalCommand::Install { packages, node, force, concurrency, .. }) =
install.managed_global_command()
else {
Expand Down
22 changes: 11 additions & 11 deletions crates/vp_pm_cli/src/resolution/commands/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@ pub struct AddArgs {
pub(crate) save_dependency: SaveDependencyArgs,

/// Save exact version rather than semver range
#[arg(short = 'E', long)]
#[arg(short = 'E', long, conflicts_with = "global")]
pub(crate) save_exact: bool,

/// Save the new dependency to the specified catalog name
#[arg(long, value_name = "CATALOG_NAME", not_supported(npm, pnpm < "10.12.0", yarn, bun < "1.4"))]
#[arg(long, value_name = "CATALOG_NAME", conflicts_with = "global", not_supported(npm, pnpm < "10.12.0", yarn, bun < "1.4"))]
pub(crate) save_catalog_name: Option<String>,

/// Save the new dependency to the default catalog
#[arg(long, not_supported(npm, pnpm < "10.12.0", yarn, bun < "1.4"))]
#[arg(long, conflicts_with = "global", not_supported(npm, pnpm < "10.12.0", yarn, bun < "1.4"))]
pub(crate) save_catalog: bool,

/// A list of package names allowed to run postinstall
#[arg(long, value_name = "NAMES", not_supported(npm, pnpm < "10.4.0", yarn, bun))]
#[arg(long, value_name = "NAMES", conflicts_with = "global", not_supported(npm, pnpm < "10.4.0", yarn, bun))]
pub(crate) allow_build: Option<String>,

/// Do not run lifecycle scripts
Expand Down Expand Up @@ -82,15 +82,15 @@ pub struct AddArgs {
pub(crate) silent: bool,

/// Filter packages in monorepo (can be used multiple times)
#[arg(long, value_name = "PATTERN", not_supported(npm < "7.14.0", yarn < "2", bun < "1.4"))]
#[arg(long, value_name = "PATTERN", conflicts_with = "global", not_supported(npm < "7.14.0", yarn < "2", bun < "1.4"))]
pub(crate) filter: Vec<String>,

/// Add to workspace root
#[arg(short = 'w', long, not_supported(npm < "8.1.0", yarn >= "2", bun))]
#[arg(short = 'w', long, conflicts_with = "global", not_supported(npm < "8.1.0", yarn >= "2", bun))]
pub(crate) workspace_root: bool,

/// Only add if package exists in workspace (pnpm-specific)
#[arg(long, not_supported(npm, yarn, bun))]
#[arg(long, conflicts_with = "global", not_supported(npm, yarn, bun))]
pub(crate) workspace: bool,

/// Install globally
Expand Down Expand Up @@ -126,19 +126,19 @@ pub(crate) enum SaveDependencyTarget {
#[group(id = "save_dependency_target", multiple = false)]
pub(crate) struct SaveDependencyArgs {
/// Save to `dependencies` (default)
#[arg(short = 'P', long)]
#[arg(short = 'P', long, conflicts_with = "global")]
pub(crate) save_prod: bool,

/// Save to `devDependencies`
#[arg(short = 'D', long)]
#[arg(short = 'D', long, conflicts_with = "global")]
pub(crate) save_dev: bool,

/// Save to `peerDependencies` and `devDependencies`
#[arg(long)]
#[arg(long, conflicts_with = "global")]
pub(crate) save_peer: bool,

/// Save to `optionalDependencies`
#[arg(short = 'O', long)]
#[arg(short = 'O', long, conflicts_with = "global")]
pub(crate) save_optional: bool,
}

Expand Down
Loading
Loading