Skip to content

Commit bdae5d4

Browse files
authored
feat(pm): add vp pm approve-builds subcommand (#1662)
## Summary Adds `vp pm approve-builds` — a unified subcommand for approving dependency lifecycle scripts (`install`/`postinstall`). Mirrors `pnpm approve-builds` one-to-one, adapts to `bun pm trust`, and falls back to informative warn-and-noop on npm/yarn. **Surface** (intentionally tight, matches pnpm's documented flags): ```bash vp pm approve-builds # interactive (pnpm) vp pm approve-builds esbuild fsevents # approve named packages vp pm approve-builds esbuild !core-js # pnpm >= 11.0.0 (deny syntax) vp pm approve-builds --all # pnpm >= 10.32.0 / bun vp pm approve-builds -- <raw args> # forward to underlying PM ``` ## Cross-PM behavior | PM | Behavior | |---|---| | **pnpm** | Pass-through. `--all` gated on `>= 10.32.0`, `!pkg` deny syntax gated on `>= 11.0.0` (per pnpm PR #11030). Prereleases (`10.32.0-rc.0`, `11.0.0-beta.1`) satisfy via `Version::parse("<floor>-0")` comparison. | | **bun** | `bun pm trust [--all] [pkgs...]`. `!pkg` tokens emit a warn and are filtered (bun has no denylist model). When only deny tokens are given, the warn alone is enough — no redundant note. | | **npm** | Warn + exit 0, pointing at `ignore-scripts=true` in `.npmrc`. | | **yarn** | Warn + exit 0. **Yarn 1** (Classic) gets an npm-style hint (lifecycle scripts run by default); **Yarn Berry** gets `dependenciesMeta.<pkg>.built: true` advice. Per yarn docs, `enableScripts` defaults to `false` in Berry. | ## Safety - **`--all` and positional packages are mutually exclusive** at the clap layer (`conflicts_with = "packages"`). This prevents a silent override where `--all !core-js` on bun would warn "Skipping: core-js" then `bun pm trust --all` everything anyway. - Version-gate failures render via `Error::UserMessage` (no harsh `error:` prefix). - `ApproveBuilds` is **not** in the `needs_project` allowlist, so the npm/yarn/bun educational messages can fire outside a project directory. ## Files - **Code**: `crates/vite_install/src/commands/approve_builds.rs` (new), `crates/vite_install/src/commands/mod.rs` (export), `crates/vite_pm_cli/src/cli.rs` (variant + parse tests), `crates/vite_pm_cli/src/handlers.rs` (dispatch + error mapping) - **RFC**: `rfcs/approve-builds-command.md` - **Snap tests**: - Local: `command-pm-approve-builds-{pnpm10,npm,yarn}/` - Global: `command-pm-approve-builds-{bun,pnpm10-old}/` (new) - Updated: `snap-tests-global/cli-helper-message/snap.txt` (for the new subcommand entry) ## Test plan - [x] `cargo test -p vite_install --lib approve_builds` → 23/23 - [x] `cargo test -p vite_pm_cli --lib approve_builds` → 8/8 - [x] `vp check --fix` → 0 warnings/errors - [x] `pnpm -F vite-plus snap-test-local approve-builds` → 3/3 pass - [x] `pnpm -F vite-plus snap-test-global approve-builds` → 2/2 pass - [ ] CI matrix verification <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > New PM subcommand with pass-through to native tools; npm/yarn are no-ops and pnpm gates limit misuse. Touches CLI error rendering only for this path. > > **Overview** > Introduces **`vp pm approve-builds`** so one CLI can approve dependency install/postinstall scripts: **pnpm** gets `pnpm approve-builds` (including `!pkg` on ≥11 and `--all` on ≥10.32.0), **bun** gets `bun pm trust` with `!pkg` filtered and warned, and **npm/yarn** print guidance and exit 0 without shelling out. > > The resolver uses **`node-semver`** for pnpm capability checks, rejects **`--all` plus positional packages** (including via `--`), and maps version-gate failures to **`UserMessage`** so local and global CLIs show friendly errors. **`ApproveBuilds`** is wired through **`vite_pm_cli`** (clap variant, handler, project-scoped PM build) and the local CLI binding handles **`UserMessage`** like the global CLI. > > Adds an RFC, per-PM **snap tests**, updates **`vp pm -h`**, normalizes bun banner hashes in snapshots, and drops the `error:` prefix on missing **`package.json`** messages in one snap fixture. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 7d975a4. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 8a76839 commit bdae5d4

39 files changed

Lines changed: 1848 additions & 44 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/vite_install/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ flate2 = { workspace = true }
1414
futures-util = { workspace = true }
1515
hex = { workspace = true }
1616
indoc = { workspace = true }
17+
node-semver = { workspace = true }
1718
pathdiff = { workspace = true }
1819
semver = { workspace = true }
1920
serde = { workspace = true, features = ["derive"] }

crates/vite_install/src/commands/approve_builds.rs

Lines changed: 620 additions & 0 deletions
Large diffs are not rendered by default.

crates/vite_install/src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod add;
2+
pub mod approve_builds;
23
pub mod audit;
34
pub mod cache;
45
pub mod config;

crates/vite_pm_cli/src/cli.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,23 @@ impl PackageManagerCommand {
554554
/// Package manager subcommands (`vp pm <subcommand>`).
555555
#[derive(Subcommand, Debug, Clone)]
556556
pub enum PmCommands {
557+
/// Approve dependency lifecycle scripts (install/postinstall) to run
558+
#[command(name = "approve-builds")]
559+
ApproveBuilds {
560+
/// Packages to approve. Prefix with `!` to deny (pnpm >= 11.0.0 only).
561+
/// Omit to launch interactive mode (pnpm only).
562+
packages: Vec<String>,
563+
564+
/// Approve every package currently pending approval (pnpm >= 10.32.0).
565+
/// Mutually exclusive with positional packages.
566+
#[arg(long, conflicts_with = "packages")]
567+
all: bool,
568+
569+
/// Additional arguments to pass through to the package manager
570+
#[arg(last = true, allow_hyphen_values = true)]
571+
pass_through_args: Option<Vec<String>>,
572+
},
573+
557574
/// Remove unnecessary packages
558575
Prune {
559576
/// Remove devDependencies
@@ -1182,4 +1199,56 @@ mod tests {
11821199

11831200
assert_eq!(error.kind(), clap::error::ErrorKind::ValueValidation);
11841201
}
1202+
1203+
#[test]
1204+
fn approve_builds_all_conflicts_with_packages() {
1205+
let error = parse_pm_command(&["vp", "pm", "approve-builds", "--all", "esbuild"])
1206+
.expect_err("expected --all + positional to fail");
1207+
1208+
assert_eq!(error.kind(), clap::error::ErrorKind::ArgumentConflict);
1209+
}
1210+
1211+
#[test]
1212+
fn approve_builds_all_conflicts_with_deny_packages() {
1213+
let error = parse_pm_command(&["vp", "pm", "approve-builds", "--all", "!core-js"])
1214+
.expect_err("expected --all + !pkg to fail");
1215+
1216+
assert_eq!(error.kind(), clap::error::ErrorKind::ArgumentConflict);
1217+
}
1218+
1219+
#[test]
1220+
fn approve_builds_all_alone_parses() {
1221+
let command = parse_pm_command(&["vp", "pm", "approve-builds", "--all"])
1222+
.expect("--all alone should parse");
1223+
1224+
assert!(matches!(
1225+
command,
1226+
PackageManagerCommand::Pm(PmCommands::ApproveBuilds { all: true, .. })
1227+
));
1228+
}
1229+
1230+
#[test]
1231+
fn approve_builds_packages_alone_parses() {
1232+
let command = parse_pm_command(&["vp", "pm", "approve-builds", "esbuild", "fsevents"])
1233+
.expect("positional packages should parse");
1234+
1235+
assert!(matches!(
1236+
command,
1237+
PackageManagerCommand::Pm(PmCommands::ApproveBuilds { all: false, .. })
1238+
));
1239+
}
1240+
1241+
#[test]
1242+
fn approve_builds_pass_through_args_capture() {
1243+
let command =
1244+
parse_pm_command(&["vp", "pm", "approve-builds", "esbuild", "--", "--workspace-root"])
1245+
.expect("pass-through args should parse");
1246+
1247+
let PackageManagerCommand::Pm(PmCommands::ApproveBuilds { pass_through_args, .. }) =
1248+
command
1249+
else {
1250+
panic!("expected ApproveBuilds variant");
1251+
};
1252+
assert_eq!(pass_through_args, Some(vec!["--workspace-root".to_string()]));
1253+
}
11851254
}

crates/vite_pm_cli/src/handlers.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use vite_install::{
88
PackageManager,
99
commands::{
1010
add::AddCommandOptions,
11+
approve_builds::ApproveBuildsCommandOptions,
1112
audit::AuditCommandOptions,
1213
cache::CacheCommandOptions,
1314
config::ConfigCommandOptions,
@@ -178,7 +179,8 @@ pub async fn run_pm_subcommand(
178179
) -> Result<ExitStatus, Error> {
179180
let needs_project = matches!(
180181
command,
181-
PmCommands::Prune { .. }
182+
PmCommands::ApproveBuilds { .. }
183+
| PmCommands::Prune { .. }
182184
| PmCommands::Pack { .. }
183185
| PmCommands::List { .. }
184186
| PmCommands::Publish { .. }
@@ -194,6 +196,21 @@ pub async fn run_pm_subcommand(
194196
};
195197

196198
match command {
199+
PmCommands::ApproveBuilds { packages, all, pass_through_args } => {
200+
let options = ApproveBuildsCommandOptions {
201+
packages: &packages,
202+
all,
203+
pass_through_args: pass_through_args.as_deref(),
204+
};
205+
// Map `Error::InvalidArgument` from the resolver to `UserMessage`
206+
// so the version-gate failure renders without the harsh `error:` prefix.
207+
match pm.run_approve_builds_command(&options, cwd).await {
208+
Ok(status) => Ok(status),
209+
Err(vite_error::Error::InvalidArgument(msg)) => Err(Error::UserMessage(msg)),
210+
Err(other) => Err(Error::Install(other)),
211+
}
212+
}
213+
197214
PmCommands::Prune { prod, no_optional, pass_through_args } => {
198215
let options = PruneCommandOptions {
199216
prod,

packages/cli/binding/src/cli/mod.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,9 +208,17 @@ async fn execute_pm_command(
208208
"Global package operations (`-g`/`--global`) are only supported by the globally-installed `vp` CLI. See https://viteplus.dev/guide/ to install it, then run the same command via the global `vp` binary.",
209209
)));
210210
}
211-
let status = vite_pm_cli::dispatch(cwd, command)
212-
.await
213-
.map_err(|e| Error::Anyhow(anyhow::Error::new(e)))?;
211+
let status = match vite_pm_cli::dispatch(cwd, command).await {
212+
Ok(status) => status,
213+
// Render `UserMessage` cleanly (no `error:` prefix) and exit non-zero —
214+
// matches the global CLI's `is_user_message()` branch in main.rs so the
215+
// friendly version-gate / usage errors look the same on both surfaces.
216+
Err(vite_pm_cli::Error::UserMessage(msg)) => {
217+
vite_shared::output::raw_stderr(&msg);
218+
return Ok(ExitStatus(1));
219+
}
220+
Err(e) => return Err(Error::Anyhow(anyhow::Error::new(e))),
221+
};
214222
Ok(ExitStatus(status.code().unwrap_or(1) as u8))
215223
}
216224

packages/cli/snap-tests-global/cli-helper-message/snap.txt

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -313,25 +313,26 @@ Usage: vp pm <COMMAND>
313313
Forward a command to the package manager
314314

315315
Commands:
316-
prune Remove unnecessary packages
317-
pack Create a tarball of the package
318-
list List installed packages [aliases: ls]
319-
view View package information from the registry [aliases: info, show]
320-
publish Publish package to registry
321-
owner Manage package owners [aliases: author]
322-
cache Manage package cache
323-
config Manage package manager configuration [aliases: c]
324-
login Log in to a registry [aliases: adduser]
325-
logout Log out from a registry
326-
whoami Show the current logged-in user
327-
token Manage authentication tokens
328-
audit Run a security audit
329-
dist-tag Manage distribution tags
330-
deprecate Deprecate a package version
331-
search Search for packages in the registry
332-
rebuild Rebuild native modules [aliases: rb]
333-
fund Show funding information for installed packages
334-
ping Ping the registry
316+
approve-builds Approve dependency lifecycle scripts (install/postinstall) to run
317+
prune Remove unnecessary packages
318+
pack Create a tarball of the package
319+
list List installed packages [aliases: ls]
320+
view View package information from the registry [aliases: info, show]
321+
publish Publish package to registry
322+
owner Manage package owners [aliases: author]
323+
cache Manage package cache
324+
config Manage package manager configuration [aliases: c]
325+
login Log in to a registry [aliases: adduser]
326+
logout Log out from a registry
327+
whoami Show the current logged-in user
328+
token Manage authentication tokens
329+
audit Run a security audit
330+
dist-tag Manage distribution tags
331+
deprecate Deprecate a package version
332+
search Search for packages in the registry
333+
rebuild Rebuild native modules [aliases: rb]
334+
fund Show funding information for installed packages
335+
ping Ping the registry
335336

336337
Options:
337338
-h, --help Print help

packages/cli/snap-tests-global/command-add-bun/snap.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Usage: vp add <PACKAGES>... [-- <PASS_THROUGH_ARGS>...]
3636
For more information, try '--help'.
3737

3838
> vp add testnpm2 -D && cat package.json # should add package as dev dependencies
39-
bun add v<semver> (af24e281)
39+
bun add v<semver> (<hash>)
4040

4141
installed testnpm2@<semver>
4242

@@ -51,7 +51,7 @@ installed testnpm2@<semver>
5151
}
5252

5353
> vp add testnpm2 test-vite-plus-install && cat package.json # should add packages to dependencies
54-
bun add v<semver> (af24e281)
54+
bun add v<semver> (<hash>)
5555

5656
installed testnpm2@<semver>
5757
installed test-vite-plus-install@<semver>
@@ -70,7 +70,7 @@ installed test-vite-plus-install@<semver>
7070
}
7171

7272
> vp install test-vite-plus-package@1.0.0 --save-peer && cat package.json # should install package alias for add
73-
bun add v<semver> (af24e281)
73+
bun add v<semver> (<hash>)
7474

7575
installed test-vite-plus-package@<semver>
7676

@@ -91,7 +91,7 @@ installed test-vite-plus-package@<semver>
9191
}
9292

9393
> vp add test-vite-plus-package-optional -O && cat package.json # should add package as optional dependencies
94-
bun add v<semver> (af24e281)
94+
bun add v<semver> (<hash>)
9595

9696
installed test-vite-plus-package-optional@<semver>
9797

packages/cli/snap-tests-global/command-list-bun/snap.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
> vp install # should install packages first
2-
bun install v<semver> (af24e281)
2+
bun install v<semver> (<hash>)
33

44
+ test-vite-plus-package@<semver>
55
+ test-vite-plus-package-optional@<semver>

0 commit comments

Comments
 (0)