feat(cli): bring all package-manager commands to the local CLI#1495
feat(cli): bring all package-manager commands to the local CLI#1495fengmk2 wants to merge 8 commits intovp-create-support-orgfrom
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
How to use the Graphite Merge QueueAdd the label auto-merge to this PR to add it to the merge queue. You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has enabled the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. This stack of pull requests is managed by Graphite. Learn more about stacking. |
59bf6a6 to
6965827
Compare
|
@cursor review |
`managed_update` was delegating to `managed_install` after the recent
extract, which made `vp update -g` print "Failed to install {pkg}" on
error instead of the original "Failed to update {pkg}". Inline the
loop in `managed_update` so the verb matches the user's command.
Also drop unused `raw_stderr_inline` from `vite_shared::output`.
The companion `raw_stderr` is still exported and used; the inline
variant was added speculatively for symmetry with `raw_inline` but
has no callers, so YAGNI — adding it back is a one-liner if needed.
Closes two cursor bugbot findings on PR #1495.
Two local-CLI safety bugs on the new shared PM dispatcher (flagged by the codex adversarial review on PR #1495): 1. `vp install -g <pkg>` was downgraded into a project add. The shared `Install` arm dropped `global` and the `install <packages>` → `add` alias hardcoded `AddCommandOptions.global = false`, so the command silently ran `pnpm add <pkg>` (project) instead of `pnpm add -g <pkg>` (global). Worse, `run_add` always called `ensure_package_json`, so invoking `vp install -g foo` from an empty directory created a fresh `package.json` and added the package as a project dependency. Pass `global` through from `Install` to `AddCommandOptions`, and in `run_add` skip `ensure_package_json` when `options.global` is true, falling back to npm-default PM detection so we still have a binary to dispatch to in an empty directory. Same fix mirrored on `run_remove` so `vp remove -g <pkg>` works in an empty dir too. 2. `vp remove -g --dry-run <pkg>` could perform a real uninstall. The dispatcher bound `dry_run: _` and never forwarded it; the underlying `RemoveCommandOptions` has no preview field. Reject the combination in the dispatcher with a clear user message pointing the user at the globally-installed `vp` CLI. The global CLI's `run_package_manager_command` intercepts `Remove { global: true }` before reaching this path, so it keeps using its vite-plus-managed `managed_uninstall` flow with `dry_run` honored; the new error only fires on the local CLI. Verified end-to-end on the local CLI in an empty tmp dir: - `vp install -g testnpm2` installs globally, no package.json created - `vp remove -g testnpm2` removes globally, exit 0 - `vp remove -g --dry-run testnpm2` exits 1 with the new message and does not touch the global store Snap-test deltas: only the local `command-remove-pnpm10` fixture regenerates to capture the new error message; the global fixture is unchanged because the managed-uninstall path still handles `--dry-run`.
The local CLI never had a vite-plus-managed package store. Routing `vp install/add/remove/update -g` and `vp pm list -g` through the shared `vite_pm_cli::dispatch` either silently ran the project flow (creating a fresh package.json on `vp install -g foo` in an empty directory), silently dropped flags like `--node` and `--dry-run`, or in the codex-flagged Update case mutated the user's local package.json / lockfile when they meant to update a global install. Add a one-shot rejection in the local CLI binding, scoped to exactly the cases the global CLI's `run_package_manager_command` specially handles (Install/Add/Remove/Update with `global=true`, plus `Pm::List -g`). Pass-through `-g` cases that already work the same on both CLIs (`outdated -g`, `why -g`, `pm config get -g`, etc.) are left alone — `is_managed_global()` returns `false` for them. - `crates/vite_pm_cli/src/cli.rs`: new `PackageManagerCommand::is_managed_global` helper. Shared crate stays semantically neutral; the binding decides rejection. - `packages/cli/binding/src/cli/mod.rs`: precheck in `execute_pm_command` returns a friendly error pointing the user at https://viteplus.dev/guide/ before calling `dispatch`. Exit 1 via the binding's existing JS error path. - `crates/vite_pm_cli/src/dispatch.rs`: drop the now-dead `--dry-run` rejection on Remove. Clap requires `--dry-run` alongside `-g`, the binding rejects `-g` first, and the global CLI intercepts `-g` before dispatch — so this arm only ever sees `dry_run: false`. - `crates/vite_pm_cli/src/handlers.rs`: revert the `if options.global` branches in `run_add` / `run_remove` for the same reason; they're unreachable now. - `packages/cli/snap-tests/command-pm-global-rejected/`: new local-only fixture that exercises every rejected case (install/add/remove/update -g, with --node and --dry-run combinations, plus pm ls -g). - `packages/cli/snap-tests/command-remove-pnpm10/snap.txt`: regenerated with the new full-rejection wording in place of the old dispatcher-side --dry-run-only message. snap-tests-global/ unchanged: the global CLI still hits its managed-install path before reaching the binding rejection. Verified end-to-end on the local CLI in an empty /tmp dir: - `vp install -g foo`, `vp add -g foo --node 20`, `vp remove -g foo`, `vp remove -g --dry-run foo`, `vp update -g [foo]`, `vp pm ls -g` all exit 1 with the new error and create no package.json. - `vp pm config get -g registry` (pass-through) still works on both CLIs. - `vp add lodash` (no -g) still creates a project and adds the dep. Closes the second codex adversarial review's high/medium findings on PR #1495.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 74c9564. Configure here.
Extract the package-manager CLI surface (clap definitions + dispatch glue) into a new shared crate `vite_pm_cli` consumed by both the global Rust binary and the local NAPI binding, so `npx vp add <pkg>`, `vp remove`, `vp update`, `vp dedupe`, `vp dlx`, `vp pm publish`, etc. all work identically on the local CLI. Previously the local CLI only knew the `install` shortcut; every other PM command fell through to clap's "unknown subcommand" error. The core PM logic in `vite_install` was already a binding dependency — only the parser and dispatcher were missing. Sharing the clap surface guarantees the two CLIs cannot drift on flag names, aliases, or behavior. The global CLI keeps a thin wrapper that intercepts `--global` for vite-plus-managed installs (`commands::env::global_install`) and ensures the managed Node runtime is on PATH before delegating; the local CLI dispatches PM commands directly through `vite_pm_cli` and bypasses the vite-task scheduler since PM operations do not need caching. Snap-test coverage: one representative pnpm10 fixture per command mirrored into `packages/cli/snap-tests/`. Pre-existing snap updates reflect the now-consistent behavior — `vp install --help` prints clap's help instead of forwarding to the underlying PM, and PM commands inside `vp run` correctly show "cache disabled".
…tting
- Replace `eprintln!("...{}: {}", x, y)` with `eprintln!("...{x}: {y}")` in
the managed install/uninstall helpers (matches the rest of the codebase's
format style).
- Reword the comments above the explicit `From<vite_pm_cli::Error>` impl
and the managed-install group to focus on the WHY (why these intentionally
break the project's "no raw eprintln" rule, why an explicit per-variant
match is required) rather than narrating history.
Tried extracting the three managed_* loops into a generic
`for_each_package(verb, packages, op)` helper, but the closure-future
lifetime escape requires HRTB / boxing / cloning, all of which made the
helper less readable than the duplication it would replace. Keeping the
three explicit functions.
Symmetric counterparts to the existing `raw` / `raw_inline` (which write to stdout). They print to stderr with no "error:" / "warn:" prefix — intended for the rare case where a caller needs an unprefixed stderr line (e.g. snap-test fixtures that expect bare error messages). First caller: `vite_global_cli::cli::managed_install` / `managed_uninstall` use `raw_stderr` instead of raw `eprintln!`, and `managed_update` uses `raw` instead of raw `println!`. This drops the three `#[allow(clippy::disallowed_macros)]` annotations from cli.rs and keeps the project's "no raw eprintln/println outside of the output module" rule intact.
…-binary RFCs The PM commands feature added a new `crates/vite_pm_cli/` crate that owns clap definitions and dispatch for every package-manager subcommand (install, add, remove, update, dedupe, outdated, why, info, link, unlink, dlx, pm <subcmd>), so both `vite_global_cli` and the local CLI's NAPI binding flatten `PackageManagerCommand` into their top-level argument parser and call `vite_pm_cli::dispatch`. - merge-global-and-local-cli.md: update the Category A description and routing diagram to show PM commands flowing through `vite_pm_cli` on both global (Rust) and local (NAPI) paths. Add changelog entry #11. - global-cli-rust-binary.md: drop the obsolete per-PM-command file entries from the "Files to create" list and add a note pointing to the shared crate.
Replace the explicit per-variant `From<vite_pm_cli::Error>` impl
(remapping UserMessage/Install/Workspace/etc. one-by-one) with an
`#[error(transparent)] PmCli(#[from] vite_pm_cli::Error)` variant and
an `Error::is_user_message()` helper that knows about both the
top-level `UserMessage(_)` and the wrapped `PmCli(UserMessage(_))`.
This drops the 12-line manual match in error.rs, localizes the
"is friendly message" semantic on `Error` itself instead of leaking
variant matches into main.rs, and makes adding future sub-error crates
a one-line change to `is_user_message`.
Also switch the two `eprintln!("{e}")` calls in main.rs that print
user-facing messages to `output::raw_stderr` for consistency with the
rest of the codebase's "go through `output::*` instead of raw print
macros" rule.
`managed_update` was delegating to `managed_install` after the recent
extract, which made `vp update -g` print "Failed to install {pkg}" on
error instead of the original "Failed to update {pkg}". Inline the
loop in `managed_update` so the verb matches the user's command.
Also drop unused `raw_stderr_inline` from `vite_shared::output`.
The companion `raw_stderr` is still exported and used; the inline
variant was added speculatively for symmetry with `raw_inline` but
has no callers, so YAGNI — adding it back is a one-liner if needed.
Closes two cursor bugbot findings on PR #1495.
Two local-CLI safety bugs on the new shared PM dispatcher (flagged by the codex adversarial review on PR #1495): 1. `vp install -g <pkg>` was downgraded into a project add. The shared `Install` arm dropped `global` and the `install <packages>` → `add` alias hardcoded `AddCommandOptions.global = false`, so the command silently ran `pnpm add <pkg>` (project) instead of `pnpm add -g <pkg>` (global). Worse, `run_add` always called `ensure_package_json`, so invoking `vp install -g foo` from an empty directory created a fresh `package.json` and added the package as a project dependency. Pass `global` through from `Install` to `AddCommandOptions`, and in `run_add` skip `ensure_package_json` when `options.global` is true, falling back to npm-default PM detection so we still have a binary to dispatch to in an empty directory. Same fix mirrored on `run_remove` so `vp remove -g <pkg>` works in an empty dir too. 2. `vp remove -g --dry-run <pkg>` could perform a real uninstall. The dispatcher bound `dry_run: _` and never forwarded it; the underlying `RemoveCommandOptions` has no preview field. Reject the combination in the dispatcher with a clear user message pointing the user at the globally-installed `vp` CLI. The global CLI's `run_package_manager_command` intercepts `Remove { global: true }` before reaching this path, so it keeps using its vite-plus-managed `managed_uninstall` flow with `dry_run` honored; the new error only fires on the local CLI. Verified end-to-end on the local CLI in an empty tmp dir: - `vp install -g testnpm2` installs globally, no package.json created - `vp remove -g testnpm2` removes globally, exit 0 - `vp remove -g --dry-run testnpm2` exits 1 with the new message and does not touch the global store Snap-test deltas: only the local `command-remove-pnpm10` fixture regenerates to capture the new error message; the global fixture is unchanged because the managed-uninstall path still handles `--dry-run`.
The local CLI never had a vite-plus-managed package store. Routing `vp install/add/remove/update -g` and `vp pm list -g` through the shared `vite_pm_cli::dispatch` either silently ran the project flow (creating a fresh package.json on `vp install -g foo` in an empty directory), silently dropped flags like `--node` and `--dry-run`, or in the codex-flagged Update case mutated the user's local package.json / lockfile when they meant to update a global install. Add a one-shot rejection in the local CLI binding, scoped to exactly the cases the global CLI's `run_package_manager_command` specially handles (Install/Add/Remove/Update with `global=true`, plus `Pm::List -g`). Pass-through `-g` cases that already work the same on both CLIs (`outdated -g`, `why -g`, `pm config get -g`, etc.) are left alone — `is_managed_global()` returns `false` for them. - `crates/vite_pm_cli/src/cli.rs`: new `PackageManagerCommand::is_managed_global` helper. Shared crate stays semantically neutral; the binding decides rejection. - `packages/cli/binding/src/cli/mod.rs`: precheck in `execute_pm_command` returns a friendly error pointing the user at https://viteplus.dev/guide/ before calling `dispatch`. Exit 1 via the binding's existing JS error path. - `crates/vite_pm_cli/src/dispatch.rs`: drop the now-dead `--dry-run` rejection on Remove. Clap requires `--dry-run` alongside `-g`, the binding rejects `-g` first, and the global CLI intercepts `-g` before dispatch — so this arm only ever sees `dry_run: false`. - `crates/vite_pm_cli/src/handlers.rs`: revert the `if options.global` branches in `run_add` / `run_remove` for the same reason; they're unreachable now. - `packages/cli/snap-tests/command-pm-global-rejected/`: new local-only fixture that exercises every rejected case (install/add/remove/update -g, with --node and --dry-run combinations, plus pm ls -g). - `packages/cli/snap-tests/command-remove-pnpm10/snap.txt`: regenerated with the new full-rejection wording in place of the old dispatcher-side --dry-run-only message. snap-tests-global/ unchanged: the global CLI still hits its managed-install path before reaching the binding rejection. Verified end-to-end on the local CLI in an empty /tmp dir: - `vp install -g foo`, `vp add -g foo --node 20`, `vp remove -g foo`, `vp remove -g --dry-run foo`, `vp update -g [foo]`, `vp pm ls -g` all exit 1 with the new error and create no package.json. - `vp pm config get -g registry` (pass-through) still works on both CLIs. - `vp add lodash` (no -g) still creates a project and adds the dep. Closes the second codex adversarial review's high/medium findings on PR #1495.
2ce359a to
4e511a3
Compare
74c9564 to
46f67b1
Compare

Extract the package-manager CLI surface (clap definitions + dispatch
glue) into a new shared crate
vite_pm_cliconsumed by both the globalRust binary and the local NAPI binding, so
npx vp add <pkg>,vp remove,vp update,vp dedupe,vp dlx,vp pm publish, etc.all work identically on the local CLI.
Previously the local CLI only knew the
installshortcut; every otherPM command fell through to clap's "unknown subcommand" error. The core
PM logic in
vite_installwas already a binding dependency — only theparser and dispatcher were missing. Sharing the clap surface guarantees
the two CLIs cannot drift on flag names, aliases, or behavior.
The global CLI keeps a thin wrapper that intercepts
--globalforvite-plus-managed installs (
commands::env::global_install) andensures the managed Node runtime is on PATH before delegating; the
local CLI dispatches PM commands directly through
vite_pm_cliandbypasses the vite-task scheduler since PM operations do not need
caching.
Snap-test coverage: one representative pnpm10 fixture per command
mirrored into
packages/cli/snap-tests/. Pre-existing snap updatesreflect the now-consistent behavior —
vp install --helpprintsclap's help instead of forwarding to the underlying PM, and PM
commands inside
vp runcorrectly show "cache disabled".Note
Cursor Bugbot is generating a summary for commit 2f92ac7. Configure here.