Skip to content

feat(cli): bring all package-manager commands to the local CLI#1495

Draft
fengmk2 wants to merge 8 commits intovp-create-support-orgfrom
pm-features-at-local-cli
Draft

feat(cli): bring all package-manager commands to the local CLI#1495
fengmk2 wants to merge 8 commits intovp-create-support-orgfrom
pm-features-at-local-cli

Conversation

@fengmk2
Copy link
Copy Markdown
Member

@fengmk2 fengmk2 commented Apr 29, 2026

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".


Note

Cursor Bugbot is generating a summary for commit 2f92ac7. Configure here.

Copy link
Copy Markdown
Member Author

fengmk2 commented Apr 29, 2026

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.
Learn more


How to use the Graphite Merge Queue

Add 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.

@fengmk2 fengmk2 force-pushed the pm-features-at-local-cli branch 2 times, most recently from 59bf6a6 to 6965827 Compare April 29, 2026 05:29
@fengmk2
Copy link
Copy Markdown
Member Author

fengmk2 commented Apr 29, 2026

@cursor review

Comment thread crates/vite_global_cli/src/cli.rs Outdated
Comment thread crates/vite_shared/src/output.rs Outdated
fengmk2 added a commit that referenced this pull request Apr 29, 2026
`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.
fengmk2 added a commit that referenced this pull request Apr 29, 2026
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`.
fengmk2 added a commit that referenced this pull request Apr 29, 2026
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.
Copy link
Copy Markdown
Member Author

fengmk2 commented Apr 29, 2026

@cursor review

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

fengmk2 added 8 commits April 30, 2026 10:11
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.
@fengmk2 fengmk2 force-pushed the vp-create-support-org branch from 2ce359a to 4e511a3 Compare April 30, 2026 01:11
@fengmk2 fengmk2 force-pushed the pm-features-at-local-cli branch from 74c9564 to 46f67b1 Compare April 30, 2026 01:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant