From 1227bce5c9a02c669b3dc9afc35ded43d6bab9d6 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Thu, 7 May 2026 06:50:47 +0000 Subject: [PATCH] feat(cli): bring all package-manager commands to the local CLI (#1495) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 `, `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](https://cursor.com/bugbot) is generating a summary for commit 2f92ac77a3bc1bf77c9ac630004fc77f78357c67. Configure [here](https://www.cursor.com/dashboard/bugbot). --- Cargo.lock | 19 + Cargo.toml | 1 + crates/vite_global_cli/Cargo.toml | 1 + crates/vite_global_cli/src/cli.rs | 1598 ++--------------- crates/vite_global_cli/src/commands/add.rs | 76 - crates/vite_global_cli/src/commands/dedupe.rs | 51 - crates/vite_global_cli/src/commands/dlx.rs | 87 - .../vite_global_cli/src/commands/install.rs | 133 -- crates/vite_global_cli/src/commands/link.rs | 51 - crates/vite_global_cli/src/commands/mod.rs | 99 +- .../vite_global_cli/src/commands/outdated.rs | 77 - crates/vite_global_cli/src/commands/remove.rs | 68 - crates/vite_global_cli/src/commands/unlink.rs | 52 - crates/vite_global_cli/src/commands/update.rs | 75 - crates/vite_global_cli/src/commands/vpx.rs | 17 +- crates/vite_global_cli/src/commands/why.rs | 81 - crates/vite_global_cli/src/error.rs | 11 + crates/vite_global_cli/src/main.rs | 8 +- crates/vite_pm_cli/Cargo.toml | 24 + crates/vite_pm_cli/src/cli.rs | 1117 ++++++++++++ crates/vite_pm_cli/src/dispatch.rs | 315 ++++ crates/vite_pm_cli/src/error.rs | 29 + .../pm.rs => vite_pm_cli/src/handlers.rs} | 271 ++- crates/vite_pm_cli/src/helpers.rs | 71 + crates/vite_pm_cli/src/lib.rs | 19 + crates/vite_shared/src/output.rs | 6 + packages/cli/binding/Cargo.toml | 1 + packages/cli/binding/src/check/mod.rs | 6 +- packages/cli/binding/src/cli/execution.rs | 17 +- packages/cli/binding/src/cli/handler.rs | 8 +- packages/cli/binding/src/cli/mod.rs | 28 +- packages/cli/binding/src/cli/resolver.rs | 30 +- packages/cli/binding/src/cli/types.rs | 11 +- .../command-add-pnpm10/package.json | 5 + .../snap-tests/command-add-pnpm10/snap.txt | 161 ++ .../snap-tests/command-add-pnpm10/steps.json | 12 + .../command-dedupe-pnpm10/package.json | 14 + .../snap-tests/command-dedupe-pnpm10/snap.txt | 124 ++ .../command-dedupe-pnpm10/steps.json | 10 + .../command-dlx-pnpm10/package.json | 5 + .../snap-tests/command-dlx-pnpm10/snap.txt | 33 + .../snap-tests/command-dlx-pnpm10/steps.json | 7 + .../command-info-no-package-json/snap.txt | 5 + .../command-info-no-package-json/steps.json | 6 + .../snap.txt | 24 + .../steps.json | 8 + .../command-install-shortcut/snap.txt | 8 +- .../command-link-pnpm10/package.json | 8 + .../snap-tests/command-link-pnpm10/snap.txt | 111 ++ .../snap-tests/command-link-pnpm10/steps.json | 11 + .../command-outdated-pnpm10/package.json | 14 + .../command-outdated-pnpm10/snap.txt | 173 ++ .../command-outdated-pnpm10/steps.json | 22 + .../command-pm-global-rejected/package.json | 6 + .../command-pm-global-rejected/snap.txt | 26 + .../command-pm-global-rejected/steps.json | 13 + .../command-pm-no-package-json/snap.txt | 11 + .../command-pm-no-package-json/steps.json | 8 + .../command-remove-pnpm10/package.json | 5 + .../snap-tests/command-remove-pnpm10/snap.txt | 106 ++ .../command-remove-pnpm10/steps.json | 13 + .../command-unlink-pnpm10/package.json | 5 + .../snap-tests/command-unlink-pnpm10/snap.txt | 53 + .../command-unlink-pnpm10/steps.json | 11 + .../command-update-pnpm10/package.json | 14 + .../snap-tests/command-update-pnpm10/snap.txt | 175 ++ .../command-update-pnpm10/steps.json | 16 + .../command-why-pnpm10/package.json | 14 + .../snap-tests/command-why-pnpm10/snap.txt | 142 ++ .../snap-tests/command-why-pnpm10/steps.json | 20 + .../npm-install-with-options/snap.txt | 61 +- .../npm-install-with-options/vite.config.ts | 2 +- .../vite-task-path-env-include-pm/snap.txt | 2 - .../yarn-install-with-options/snap.txt | 103 +- rfcs/global-cli-rust-binary.md | 15 +- rfcs/merge-global-and-local-cli.md | 21 +- 76 files changed, 3477 insertions(+), 2584 deletions(-) delete mode 100644 crates/vite_global_cli/src/commands/add.rs delete mode 100644 crates/vite_global_cli/src/commands/dedupe.rs delete mode 100644 crates/vite_global_cli/src/commands/dlx.rs delete mode 100644 crates/vite_global_cli/src/commands/install.rs delete mode 100644 crates/vite_global_cli/src/commands/link.rs delete mode 100644 crates/vite_global_cli/src/commands/outdated.rs delete mode 100644 crates/vite_global_cli/src/commands/remove.rs delete mode 100644 crates/vite_global_cli/src/commands/unlink.rs delete mode 100644 crates/vite_global_cli/src/commands/update.rs delete mode 100644 crates/vite_global_cli/src/commands/why.rs create mode 100644 crates/vite_pm_cli/Cargo.toml create mode 100644 crates/vite_pm_cli/src/cli.rs create mode 100644 crates/vite_pm_cli/src/dispatch.rs create mode 100644 crates/vite_pm_cli/src/error.rs rename crates/{vite_global_cli/src/commands/pm.rs => vite_pm_cli/src/handlers.rs} (58%) create mode 100644 crates/vite_pm_cli/src/helpers.rs create mode 100644 crates/vite_pm_cli/src/lib.rs create mode 100644 packages/cli/snap-tests/command-add-pnpm10/package.json create mode 100644 packages/cli/snap-tests/command-add-pnpm10/snap.txt create mode 100644 packages/cli/snap-tests/command-add-pnpm10/steps.json create mode 100644 packages/cli/snap-tests/command-dedupe-pnpm10/package.json create mode 100644 packages/cli/snap-tests/command-dedupe-pnpm10/snap.txt create mode 100644 packages/cli/snap-tests/command-dedupe-pnpm10/steps.json create mode 100644 packages/cli/snap-tests/command-dlx-pnpm10/package.json create mode 100644 packages/cli/snap-tests/command-dlx-pnpm10/snap.txt create mode 100644 packages/cli/snap-tests/command-dlx-pnpm10/steps.json create mode 100644 packages/cli/snap-tests/command-info-no-package-json/snap.txt create mode 100644 packages/cli/snap-tests/command-info-no-package-json/steps.json create mode 100644 packages/cli/snap-tests/command-install-auto-create-package-json/snap.txt create mode 100644 packages/cli/snap-tests/command-install-auto-create-package-json/steps.json create mode 100644 packages/cli/snap-tests/command-link-pnpm10/package.json create mode 100644 packages/cli/snap-tests/command-link-pnpm10/snap.txt create mode 100644 packages/cli/snap-tests/command-link-pnpm10/steps.json create mode 100644 packages/cli/snap-tests/command-outdated-pnpm10/package.json create mode 100644 packages/cli/snap-tests/command-outdated-pnpm10/snap.txt create mode 100644 packages/cli/snap-tests/command-outdated-pnpm10/steps.json create mode 100644 packages/cli/snap-tests/command-pm-global-rejected/package.json create mode 100644 packages/cli/snap-tests/command-pm-global-rejected/snap.txt create mode 100644 packages/cli/snap-tests/command-pm-global-rejected/steps.json create mode 100644 packages/cli/snap-tests/command-pm-no-package-json/snap.txt create mode 100644 packages/cli/snap-tests/command-pm-no-package-json/steps.json create mode 100644 packages/cli/snap-tests/command-remove-pnpm10/package.json create mode 100644 packages/cli/snap-tests/command-remove-pnpm10/snap.txt create mode 100644 packages/cli/snap-tests/command-remove-pnpm10/steps.json create mode 100644 packages/cli/snap-tests/command-unlink-pnpm10/package.json create mode 100644 packages/cli/snap-tests/command-unlink-pnpm10/snap.txt create mode 100644 packages/cli/snap-tests/command-unlink-pnpm10/steps.json create mode 100644 packages/cli/snap-tests/command-update-pnpm10/package.json create mode 100644 packages/cli/snap-tests/command-update-pnpm10/snap.txt create mode 100644 packages/cli/snap-tests/command-update-pnpm10/steps.json create mode 100644 packages/cli/snap-tests/command-why-pnpm10/package.json create mode 100644 packages/cli/snap-tests/command-why-pnpm10/snap.txt create mode 100644 packages/cli/snap-tests/command-why-pnpm10/steps.json diff --git a/Cargo.lock b/Cargo.lock index a6c0abcbf8..1ce7604faa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7432,6 +7432,7 @@ dependencies = [ "vite_install", "vite_migration", "vite_path", + "vite_pm_cli", "vite_shared", "vite_static_config", "vite_str", @@ -7511,6 +7512,7 @@ dependencies = [ "vite_install", "vite_js_runtime", "vite_path", + "vite_pm_cli", "vite_setup", "vite_shared", "vite_str", @@ -7630,6 +7632,23 @@ dependencies = [ "wincode", ] +[[package]] +name = "vite_pm_cli" +version = "0.0.0" +dependencies = [ + "clap", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tracing", + "vite_command", + "vite_error", + "vite_install", + "vite_path", + "vite_str", + "vite_workspace", +] + [[package]] name = "vite_select" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index 4f8254afab..2ee44c0d29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -197,6 +197,7 @@ vite_js_runtime = { path = "crates/vite_js_runtime" } vite_glob = { git = "https://github.com/voidzero-dev/vite-task.git", rev = "d1b8cdae8b6df5eab8b9f1143ceb4fb13933a5ef" } vite_install = { path = "crates/vite_install" } vite_migration = { path = "crates/vite_migration" } +vite_pm_cli = { path = "crates/vite_pm_cli" } vite_setup = { path = "crates/vite_setup" } vite_shared = { path = "crates/vite_shared" } vite_static_config = { path = "crates/vite_static_config" } diff --git a/crates/vite_global_cli/Cargo.toml b/crates/vite_global_cli/Cargo.toml index 55d1f3d333..90eecf1648 100644 --- a/crates/vite_global_cli/Cargo.toml +++ b/crates/vite_global_cli/Cargo.toml @@ -28,6 +28,7 @@ crossterm = { workspace = true } vite_error = { workspace = true } vite_install = { workspace = true } vite_js_runtime = { workspace = true } +vite_pm_cli = { workspace = true } vite_path = { workspace = true } vite_command = { workspace = true } vite_setup = { workspace = true } diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index b36ea87dde..4e864f555b 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -8,19 +8,10 @@ use std::{ffi::OsStr, process::ExitStatus}; use clap::{CommandFactory, FromArgMatches, Parser, Subcommand}; use clap_complete::ArgValueCompleter; use tokio::runtime::Runtime; -use vite_install::commands::{ - add::SaveDependencyType, install::InstallCommandOptions, outdated::Format, -}; use vite_path::AbsolutePathBuf; +use vite_pm_cli::PackageManagerCommand; -use crate::{ - commands::{ - self, AddCommand, DedupeCommand, DlxCommand, InstallCommand, LinkCommand, OutdatedCommand, - RemoveCommand, UnlinkCommand, UpdateCommand, WhyCommand, - }, - error::Error, - help, -}; +use crate::{commands, error::Error, help}; #[derive(Clone, Copy, Debug)] pub struct RenderOptions { @@ -57,470 +48,11 @@ pub struct Args { pub enum Commands { // ========================================================================= // Category A: Package Manager Commands + // (clap-flattened from `vite_pm_cli::PackageManagerCommand` so the + // global CLI and the local CLI binding share an identical surface.) // ========================================================================= - /// Install all dependencies, or add packages if package names are provided - #[command(visible_alias = "i")] - Install { - /// Do not install devDependencies - #[arg(short = 'P', long)] - prod: bool, - - /// Only install devDependencies (install) / Save to devDependencies (add) - #[arg(short = 'D', long)] - dev: bool, - - /// Do not install optionalDependencies - #[arg(long)] - no_optional: bool, - - /// Fail if lockfile needs to be updated (CI mode) - #[arg(long, overrides_with = "no_frozen_lockfile")] - frozen_lockfile: bool, - - /// Allow lockfile updates (opposite of --frozen-lockfile) - #[arg(long, overrides_with = "frozen_lockfile")] - no_frozen_lockfile: bool, - - /// Only update lockfile, don't install - #[arg(long)] - lockfile_only: bool, - - /// Use cached packages when available - #[arg(long)] - prefer_offline: bool, - - /// Only use packages already in cache - #[arg(long)] - offline: bool, - - /// Force reinstall all dependencies - #[arg(short = 'f', long)] - force: bool, - - /// Do not run lifecycle scripts - #[arg(long)] - ignore_scripts: bool, - - /// Don't read or generate lockfile - #[arg(long)] - no_lockfile: bool, - - /// Fix broken lockfile entries (pnpm and yarn@2+ only) - #[arg(long)] - fix_lockfile: bool, - - /// Create flat `node_modules` (pnpm only) - #[arg(long)] - shamefully_hoist: bool, - - /// Re-run resolution for peer dependency analysis (pnpm only) - #[arg(long)] - resolution_only: bool, - - /// Suppress output (silent mode) - #[arg(long)] - silent: bool, - - /// Filter packages in monorepo (can be used multiple times) - #[arg(long, value_name = "PATTERN")] - filter: Option>, - - /// Install in workspace root only - #[arg(short = 'w', long)] - workspace_root: bool, - - /// Save exact version (only when adding packages) - #[arg(short = 'E', long)] - save_exact: bool, - - /// Save to peerDependencies (only when adding packages) - #[arg(long)] - save_peer: bool, - - /// Save to optionalDependencies (only when adding packages) - #[arg(short = 'O', long)] - save_optional: bool, - - /// Save the new dependency to the default catalog (only when adding packages) - #[arg(long)] - save_catalog: bool, - - /// Install globally (requires package names) - #[arg(short = 'g', long, requires = "packages")] - global: bool, - - /// Node.js version to use for global installation (only with -g) - #[arg(long, requires = "global")] - node: Option, - - /// Packages to add (if provided, acts as `vp add`) - #[arg(required = false)] - packages: Option>, - - /// Additional arguments to pass through to the package manager - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - - /// Add packages to dependencies - Add { - /// Save to `dependencies` (default) - #[arg(short = 'P', long)] - save_prod: bool, - - /// Save to `devDependencies` - #[arg(short = 'D', long)] - save_dev: bool, - - /// Save to `peerDependencies` and `devDependencies` - #[arg(long)] - save_peer: bool, - - /// Save to `optionalDependencies` - #[arg(short = 'O', long)] - save_optional: bool, - - /// Save exact version rather than semver range - #[arg(short = 'E', long)] - save_exact: bool, - - /// Save the new dependency to the specified catalog name - #[arg(long, value_name = "CATALOG_NAME")] - save_catalog_name: Option, - - /// Save the new dependency to the default catalog - #[arg(long)] - save_catalog: bool, - - /// A list of package names allowed to run postinstall - #[arg(long, value_name = "NAMES")] - allow_build: Option, - - /// Filter packages in monorepo (can be used multiple times) - #[arg(long, value_name = "PATTERN")] - filter: Option>, - - /// Add to workspace root - #[arg(short = 'w', long)] - workspace_root: bool, - - /// Only add if package exists in workspace (pnpm-specific) - #[arg(long)] - workspace: bool, - - /// Install globally - #[arg(short = 'g', long)] - global: bool, - - /// Node.js version to use for global installation (only with -g) - #[arg(long, requires = "global")] - node: Option, - - /// Packages to add - #[arg(required = true)] - packages: Vec, - - /// Additional arguments to pass through to the package manager - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - - /// Remove packages from dependencies - #[command(visible_alias = "rm", visible_alias = "un", visible_alias = "uninstall")] - Remove { - /// Only remove from `devDependencies` (pnpm-specific) - #[arg(short = 'D', long)] - save_dev: bool, - - /// Only remove from `optionalDependencies` (pnpm-specific) - #[arg(short = 'O', long)] - save_optional: bool, - - /// Only remove from `dependencies` (pnpm-specific) - #[arg(short = 'P', long)] - save_prod: bool, - - /// Filter packages in monorepo (can be used multiple times) - #[arg(long, value_name = "PATTERN")] - filter: Option>, - - /// Remove from workspace root - #[arg(short = 'w', long)] - workspace_root: bool, - - /// Remove recursively from all workspace packages - #[arg(short = 'r', long)] - recursive: bool, - - /// Remove global packages - #[arg(short = 'g', long)] - global: bool, - - /// Preview what would be removed without actually removing (only with -g) - #[arg(long, requires = "global")] - dry_run: bool, - - /// Packages to remove - #[arg(required = true)] - packages: Vec, - - /// Additional arguments to pass through to the package manager - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - - /// Update packages to their latest versions - #[command(visible_alias = "up")] - Update { - /// Update to latest version (ignore semver range) - #[arg(short = 'L', long)] - latest: bool, - - /// Update global packages - #[arg(short = 'g', long)] - global: bool, - - /// Update recursively in all workspace packages - #[arg(short = 'r', long)] - recursive: bool, - - /// Filter packages in monorepo (can be used multiple times) - #[arg(long, value_name = "PATTERN")] - filter: Option>, - - /// Include workspace root - #[arg(short = 'w', long)] - workspace_root: bool, - - /// Update only devDependencies - #[arg(short = 'D', long)] - dev: bool, - - /// Update only dependencies (production) - #[arg(short = 'P', long)] - prod: bool, - - /// Interactive mode - #[arg(short = 'i', long)] - interactive: bool, - - /// Don't update optionalDependencies - #[arg(long)] - no_optional: bool, - - /// Update lockfile only, don't modify package.json - #[arg(long)] - no_save: bool, - - /// Only update if package exists in workspace (pnpm-specific) - #[arg(long)] - workspace: bool, - - /// Packages to update (optional - updates all if omitted) - packages: Vec, - - /// Additional arguments to pass through to the package manager - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - - /// Deduplicate dependencies - Dedupe { - /// Check if deduplication would make changes - #[arg(long)] - check: bool, - - /// Additional arguments to pass through to the package manager - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - - /// Check for outdated packages - Outdated { - /// Package name(s) to check - packages: Vec, - - /// Show extended information - #[arg(long)] - long: bool, - - /// Output format: table (default), list, or json - #[arg(long, value_name = "FORMAT", value_parser = clap::value_parser!(Format))] - format: Option, - - /// Check recursively across all workspaces - #[arg(short = 'r', long)] - recursive: bool, - - /// Filter packages in monorepo - #[arg(long, value_name = "PATTERN")] - filter: Option>, - - /// Include workspace root - #[arg(short = 'w', long)] - workspace_root: bool, - - /// Only production and optional dependencies - #[arg(short = 'P', long)] - prod: bool, - - /// Only dev dependencies - #[arg(short = 'D', long)] - dev: bool, - - /// Exclude optional dependencies - #[arg(long)] - no_optional: bool, - - /// Only show compatible versions - #[arg(long)] - compatible: bool, - - /// Sort results by field - #[arg(long, value_name = "FIELD")] - sort_by: Option, - - /// Check globally installed packages - #[arg(short = 'g', long)] - global: bool, - - /// Additional arguments to pass through to the package manager - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - - /// Show why a package is installed - #[command(visible_alias = "explain")] - Why { - /// Package(s) to check - #[arg(required = true)] - packages: Vec, - - /// Output in JSON format - #[arg(long)] - json: bool, - - /// Show extended information - #[arg(long)] - long: bool, - - /// Show parseable output - #[arg(long)] - parseable: bool, - - /// Check recursively across all workspaces - #[arg(short = 'r', long)] - recursive: bool, - - /// Filter packages in monorepo - #[arg(long, value_name = "PATTERN")] - filter: Option>, - - /// Check in workspace root - #[arg(short = 'w', long)] - workspace_root: bool, - - /// Only production dependencies - #[arg(short = 'P', long)] - prod: bool, - - /// Only dev dependencies - #[arg(short = 'D', long)] - dev: bool, - - /// Limit tree depth - #[arg(long)] - depth: Option, - - /// Exclude optional dependencies - #[arg(long)] - no_optional: bool, - - /// Check globally installed packages - #[arg(short = 'g', long)] - global: bool, - - /// Exclude peer dependencies - #[arg(long)] - exclude_peers: bool, - - /// Use a finder function defined in .pnpmfile.cjs - #[arg(long, value_name = "FINDER_NAME")] - find_by: Option, - - /// Additional arguments to pass through to the package manager - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - - /// View package information from the registry - #[command(visible_alias = "view", visible_alias = "show")] - Info { - /// Package name with optional version - #[arg(required = true)] - package: String, - - /// Specific field to view - field: Option, - - /// Output in JSON format - #[arg(long)] - json: bool, - - /// Additional arguments to pass through to the package manager - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - - /// Link packages for local development - #[command(visible_alias = "ln")] - Link { - /// Package name or directory to link - #[arg(value_name = "PACKAGE|DIR")] - package: Option, - - /// Arguments to pass to package manager - #[arg(allow_hyphen_values = true, trailing_var_arg = true)] - args: Vec, - }, - - /// Unlink packages - Unlink { - /// Package name to unlink - #[arg(value_name = "PACKAGE|DIR")] - package: Option, - - /// Unlink in every workspace package - #[arg(short = 'r', long)] - recursive: bool, - - /// Arguments to pass to package manager - #[arg(allow_hyphen_values = true, trailing_var_arg = true)] - args: Vec, - }, - - /// Execute a package binary without installing it - Dlx { - /// Package(s) to install before running - #[arg(long, short = 'p', value_name = "NAME")] - package: Vec, - - /// Execute within a shell environment - #[arg(long = "shell-mode", short = 'c')] - shell_mode: bool, - - /// Suppress all output except the executed command's output - #[arg(long, short = 's')] - silent: bool, - - /// Package to execute and arguments - #[arg(required = true, trailing_var_arg = true, allow_hyphen_values = true)] - args: Vec, - }, - - /// Forward a command to the package manager - #[command(subcommand)] - Pm(PmCommands), + #[command(flatten)] + PackageManager(PackageManagerCommand), // ========================================================================= // Category B: JS Script Commands @@ -695,22 +227,11 @@ impl Commands { /// machine-readable output (--silent, -s, --json, --parseable, --format json/list). pub fn is_quiet_or_machine_readable(&self) -> bool { match self { - Self::Install { silent, .. } - | Self::Dlx { silent, .. } - | Self::Upgrade { silent, .. } => *silent, - - Self::Outdated { format, .. } => { - matches!(format, Some(Format::Json | Format::List)) - } - - Self::Why { json, parseable, .. } => *json || *parseable, - Self::Info { json, .. } => *json, - - Self::Pm(sub) => sub.is_quiet_or_machine_readable(), + Self::PackageManager(pm) => pm.is_quiet_or_machine_readable(), + Self::Upgrade { silent, .. } => *silent, Self::Env(args) => { args.command.as_ref().is_some_and(|sub| sub.is_quiet_or_machine_readable()) } - _ => false, } } @@ -924,668 +445,154 @@ pub enum SortingMethod { Desc, } -/// Package manager subcommands -#[derive(Subcommand, Debug, Clone)] -pub enum PmCommands { - /// Remove unnecessary packages - Prune { - /// Remove devDependencies - #[arg(long)] - prod: bool, +fn has_flag_before_terminator(args: &[String], flag: &str) -> bool { + for arg in args { + if arg == "--" { + break; + } + if arg == flag || arg.starts_with(&format!("{flag}=")) { + return true; + } + } + false +} - /// Remove optional dependencies - #[arg(long)] - no_optional: bool, +fn should_force_global_delegate(command: &str, args: &[String]) -> bool { + match command { + "lint" => has_flag_before_terminator(args, "--init"), + "fmt" => { + has_flag_before_terminator(args, "--init") + || has_flag_before_terminator(args, "--migrate") + } + _ => false, + } +} - /// Additional arguments - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, +/// Get available tasks for shell completion. +/// +/// Delegates to the local vite-plus CLI to run `vp run` without arguments, +/// which returns a list of available tasks in the format "task_name: description". +fn run_tasks_completions(current: &OsStr) -> Vec { + let Ok(cwd) = vite_path::current_dir() else { + return vec![]; + }; - /// Create a tarball of the package - Pack { - /// Pack all workspace packages - #[arg(short = 'r', long)] - recursive: bool, + // Unescape hashtag and trim quotes for better matching + let current = current + .to_string_lossy() + .replace("\\#", "#") + .trim_matches(|c| c == '"' || c == '\'') + .to_string(); - /// Filter packages to pack - #[arg(long, value_name = "PATTERN")] - filter: Option>, + let output = tokio::task::block_in_place(|| { + Runtime::new().ok().and_then(|rt| { + rt.block_on(async { commands::delegate::execute_output(cwd, "run", &[]).await.ok() }) + }) + }); - /// Output path for the tarball - #[arg(long)] - out: Option, + output + .filter(|o| o.status.success()) + .map(|output| { + String::from_utf8_lossy(&output.stdout) + .lines() + .filter_map(|line| line.split_once(": ").map(|(name, _)| name.trim())) + .filter(|name| !name.is_empty()) + .filter(|name| name.starts_with(¤t) || current.is_empty()) + .map(|name| clap_complete::CompletionCandidate::new(name.to_string())) + .collect() + }) + .unwrap_or_default() +} - /// Directory where the tarball will be saved - #[arg(long)] - pack_destination: Option, +/// Handle a parsed package-manager command. +/// +/// `Install`/`Add`/`Update`/`Remove` invoked with `-g`/`--global` are routed +/// through the vite-plus-managed Node.js install store (`commands::env`). +/// Everything else is forwarded to `vite_pm_cli::dispatch`, which executes +/// the underlying package manager (pnpm/npm/yarn/bun). +async fn run_package_manager_command( + cwd: AbsolutePathBuf, + command: PackageManagerCommand, +) -> Result { + match command { + PackageManagerCommand::Install { + global: true, packages: Some(pkgs), node, force, .. + } if !pkgs.is_empty() => managed_install(&pkgs, node.as_deref(), force).await, - /// Gzip compression level (0-9) - #[arg(long)] - pack_gzip_level: Option, + PackageManagerCommand::Add { global: true, ref packages, ref node, .. } => { + managed_install(packages, node.as_deref(), false).await + } - /// Output in JSON format - #[arg(long)] - json: bool, + PackageManagerCommand::Remove { global: true, ref packages, dry_run, .. } => { + managed_uninstall(packages, dry_run).await + } - /// Additional arguments - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, + PackageManagerCommand::Update { global: true, ref packages, .. } => { + managed_update(packages).await + } - /// List installed packages - #[command(visible_alias = "ls")] - List { - /// Package pattern to filter - pattern: Option, + // `pm list -g` lists vite-plus-managed globals, not the underlying PM's. + PackageManagerCommand::Pm(vite_pm_cli::cli::PmCommands::List { + global: true, + json, + ref pattern, + .. + }) => crate::commands::env::packages::execute(json, pattern.as_deref()).await, - /// Maximum depth of dependency tree - #[arg(long)] - depth: Option, + cmd => { + commands::prepend_js_runtime_to_path_env(&cwd).await?; + Ok(vite_pm_cli::dispatch(&cwd, cmd).await?) + } + } +} - /// Output in JSON format - #[arg(long)] - json: bool, - - /// Show extended information - #[arg(long)] - long: bool, - - /// Parseable output format - #[arg(long)] - parseable: bool, - - /// Only production dependencies - #[arg(short = 'P', long)] - prod: bool, - - /// Only dev dependencies - #[arg(short = 'D', long)] - dev: bool, - - /// Exclude optional dependencies - #[arg(long)] - no_optional: bool, - - /// Exclude peer dependencies - #[arg(long)] - exclude_peers: bool, - - /// Show only project packages - #[arg(long)] - only_projects: bool, - - /// Use a finder function - #[arg(long, value_name = "FINDER_NAME")] - find_by: Option, - - /// List across all workspaces - #[arg(short = 'r', long)] - recursive: bool, - - /// Filter packages in monorepo - #[arg(long, value_name = "PATTERN")] - filter: Vec, - - /// List global packages - #[arg(short = 'g', long)] - global: bool, - - /// Additional arguments - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - - /// View package information from the registry - #[command(visible_alias = "info", visible_alias = "show")] - View { - /// Package name with optional version - #[arg(required = true)] - package: String, - - /// Specific field to view - field: Option, - - /// Output in JSON format - #[arg(long)] - json: bool, - - /// Additional arguments - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - - /// Publish package to registry - Publish { - /// Tarball or folder to publish - #[arg(value_name = "TARBALL|FOLDER")] - target: Option, - - /// Preview without publishing - #[arg(long)] - dry_run: bool, - - /// Publish tag - #[arg(long)] - tag: Option, - - /// Access level (public/restricted) - #[arg(long)] - access: Option, - - /// One-time password for authentication - #[arg(long, value_name = "OTP")] - otp: Option, - - /// Skip git checks - #[arg(long)] - no_git_checks: bool, - - /// Set the branch name to publish from - #[arg(long, value_name = "BRANCH")] - publish_branch: Option, - - /// Save publish summary - #[arg(long)] - report_summary: bool, - - /// Force publish - #[arg(long)] - force: bool, - - /// Output in JSON format - #[arg(long)] - json: bool, - - /// Publish all workspace packages - #[arg(short = 'r', long)] - recursive: bool, - - /// Filter packages in monorepo - #[arg(long, value_name = "PATTERN")] - filter: Option>, - - /// Additional arguments - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - - /// Manage package owners - #[command(subcommand, visible_alias = "author")] - Owner(OwnerCommands), - - /// Manage package cache - Cache { - /// Subcommand: dir, path, clean - #[arg(required = true)] - subcommand: String, - - /// Additional arguments - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - - /// Manage package manager configuration - #[command(subcommand, visible_alias = "c")] - Config(ConfigCommands), - - /// Log in to a registry - #[command(visible_alias = "adduser")] - Login { - /// Registry URL - #[arg(long, value_name = "URL")] - registry: Option, - - /// Scope for the login - #[arg(long, value_name = "SCOPE")] - scope: Option, - - /// Additional arguments - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - - /// Log out from a registry - Logout { - /// Registry URL - #[arg(long, value_name = "URL")] - registry: Option, - - /// Scope for the logout - #[arg(long, value_name = "SCOPE")] - scope: Option, - - /// Additional arguments - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - - /// Show the current logged-in user - Whoami { - /// Registry URL - #[arg(long, value_name = "URL")] - registry: Option, - - /// Additional arguments - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - - /// Manage authentication tokens - #[command(subcommand)] - Token(TokenCommands), - - /// Run a security audit - Audit { - /// Automatically fix vulnerabilities - #[arg(long)] - fix: bool, - - /// Output in JSON format - #[arg(long)] - json: bool, - - /// Minimum vulnerability level to report - #[arg(long, value_name = "LEVEL")] - level: Option, - - /// Only audit production dependencies - #[arg(long)] - production: bool, - - /// Additional arguments - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - - /// Manage distribution tags - #[command(name = "dist-tag", subcommand)] - DistTag(DistTagCommands), - - /// Deprecate a package version - Deprecate { - /// Package name with version (e.g., "my-pkg@1.0.0") - package: String, - - /// Deprecation message - message: String, - - /// One-time password for authentication - #[arg(long, value_name = "OTP")] - otp: Option, - - /// Registry URL - #[arg(long, value_name = "URL")] - registry: Option, - - /// Additional arguments - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - - /// Search for packages in the registry - Search { - /// Search terms - #[arg(required = true, num_args = 1..)] - terms: Vec, - - /// Output in JSON format - #[arg(long)] - json: bool, - - /// Show extended information - #[arg(long)] - long: bool, - - /// Registry URL - #[arg(long, value_name = "URL")] - registry: Option, - - /// Additional arguments - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - - /// Rebuild native modules - #[command(visible_alias = "rb")] - Rebuild { - /// Additional arguments - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - - /// Show funding information for installed packages - Fund { - /// Output in JSON format - #[arg(long)] - json: bool, - - /// Additional arguments - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - - /// Ping the registry - Ping { - /// Registry URL - #[arg(long, value_name = "URL")] - registry: Option, - - /// Additional arguments - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, -} - -impl PmCommands { - fn is_quiet_or_machine_readable(&self) -> bool { - match self { - Self::List { json, parseable, .. } => *json || *parseable, - Self::Pack { json, .. } - | Self::View { json, .. } - | Self::Publish { json, .. } - | Self::Audit { json, .. } - | Self::Search { json, .. } - | Self::Fund { json, .. } => *json, - Self::Config(sub) => sub.is_quiet_or_machine_readable(), - Self::Token(sub) => sub.is_quiet_or_machine_readable(), - _ => false, +// snap-test fixtures expect bare lines (no "error:"/"info:" prefix), so +// these helpers use `output::raw_stderr`/`output::raw` rather than the +// prefixed `output::error`/`output::info`. +async fn managed_install( + packages: &[String], + node: Option<&str>, + force: bool, +) -> Result { + for package in packages { + if let Err(e) = crate::commands::env::global_install::install(package, node, force).await { + vite_shared::output::raw_stderr(&format!("Failed to install {package}: {e}")); + return Ok(exit_status(1)); } } + Ok(ExitStatus::default()) } -/// Configuration subcommands -#[derive(Subcommand, Debug, Clone)] -pub enum ConfigCommands { - /// List all configuration - List { - /// Output in JSON format - #[arg(long)] - json: bool, - - /// Use global config - #[arg(short = 'g', long)] - global: bool, - - /// Config location: project (default) or global - #[arg(long, value_name = "LOCATION")] - location: Option, - }, - - /// Get configuration value - Get { - /// Config key - key: String, - - /// Output in JSON format - #[arg(long)] - json: bool, - - /// Use global config - #[arg(short = 'g', long)] - global: bool, - - /// Config location - #[arg(long, value_name = "LOCATION")] - location: Option, - }, - - /// Set configuration value - Set { - /// Config key - key: String, - - /// Config value - value: String, - - /// Output in JSON format - #[arg(long)] - json: bool, - - /// Use global config - #[arg(short = 'g', long)] - global: bool, - - /// Config location - #[arg(long, value_name = "LOCATION")] - location: Option, - }, - - /// Delete configuration key - Delete { - /// Config key - key: String, - - /// Use global config - #[arg(short = 'g', long)] - global: bool, - - /// Config location - #[arg(long, value_name = "LOCATION")] - location: Option, - }, -} - -impl ConfigCommands { - fn is_quiet_or_machine_readable(&self) -> bool { - match self { - Self::List { json, .. } | Self::Get { json, .. } | Self::Set { json, .. } => *json, - _ => false, +async fn managed_uninstall(packages: &[String], dry_run: bool) -> Result { + for package in packages { + if let Err(e) = crate::commands::env::global_install::uninstall(package, dry_run).await { + vite_shared::output::raw_stderr(&format!("Failed to uninstall {package}: {e}")); + return Ok(exit_status(1)); } } + Ok(ExitStatus::default()) } -/// Owner subcommands -#[derive(Subcommand, Debug, Clone)] -pub enum OwnerCommands { - /// List package owners - #[command(visible_alias = "ls")] - List { - /// Package name - package: String, - - /// One-time password for authentication - #[arg(long, value_name = "OTP")] - otp: Option, - }, - - /// Add package owner - Add { - /// Username - user: String, - /// Package name - package: String, - - /// One-time password for authentication - #[arg(long, value_name = "OTP")] - otp: Option, - }, - - /// Remove package owner - Rm { - /// Username - user: String, - /// Package name - package: String, - - /// One-time password for authentication - #[arg(long, value_name = "OTP")] - otp: Option, - }, -} - -/// Token subcommands -#[derive(Subcommand, Debug, Clone)] -pub enum TokenCommands { - /// List all known tokens - #[command(visible_alias = "ls")] - List { - /// Output in JSON format - #[arg(long)] - json: bool, - - /// Registry URL - #[arg(long, value_name = "URL")] - registry: Option, - - /// Additional arguments - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, - - /// Create a new authentication token - Create { - /// Output in JSON format - #[arg(long)] - json: bool, - - /// Registry URL - #[arg(long, value_name = "URL")] - registry: Option, - - /// CIDR ranges to restrict the token to - #[arg(long, value_name = "CIDR")] - cidr: Option>, - - /// Create a read-only token - #[arg(long)] - readonly: bool, - - /// Additional arguments - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, +async fn managed_update(packages: &[String]) -> Result { + use crate::commands::env::package_metadata::PackageMetadata; - /// Revoke an authentication token - Revoke { - /// Token or token ID to revoke - token: String, - - /// Registry URL - #[arg(long, value_name = "URL")] - registry: Option, - - /// Additional arguments - #[arg(last = true, allow_hyphen_values = true)] - pass_through_args: Option>, - }, -} - -impl TokenCommands { - fn is_quiet_or_machine_readable(&self) -> bool { - match self { - Self::List { json, .. } | Self::Create { json, .. } => *json, - _ => false, + let to_update: Vec = if packages.is_empty() { + let all = PackageMetadata::list_all().await?; + if all.is_empty() { + vite_shared::output::raw("No global packages installed."); + return Ok(ExitStatus::default()); } - } -} - -/// Distribution tag subcommands -#[derive(Subcommand, Debug, Clone)] -pub enum DistTagCommands { - /// List distribution tags for a package - #[command(visible_alias = "ls")] - List { - /// Package name - package: Option, - }, - - /// Add a distribution tag - Add { - /// Package name with version (e.g., "my-pkg@1.0.0") - package_at_version: String, - - /// Tag name - tag: String, - }, - - /// Remove a distribution tag - Rm { - /// Package name - package: String, - - /// Tag name - tag: String, - }, -} - -/// Determine the save dependency type from CLI flags. -fn determine_save_dependency_type( - save_dev: bool, - save_peer: bool, - save_optional: bool, - save_prod: bool, -) -> Option { - if save_dev { - Some(SaveDependencyType::Dev) - } else if save_peer { - Some(SaveDependencyType::Peer) - } else if save_optional { - Some(SaveDependencyType::Optional) - } else if save_prod { - Some(SaveDependencyType::Production) + all.iter().map(|p| p.name.clone()).collect() } else { - None - } -} - -fn has_flag_before_terminator(args: &[String], flag: &str) -> bool { - for arg in args { - if arg == "--" { - break; - } - if arg == flag || arg.starts_with(&format!("{flag}=")) { - return true; - } - } - false -} - -fn should_force_global_delegate(command: &str, args: &[String]) -> bool { - match command { - "lint" => has_flag_before_terminator(args, "--init"), - "fmt" => { - has_flag_before_terminator(args, "--init") - || has_flag_before_terminator(args, "--migrate") + packages.to_vec() + }; + for package in &to_update { + if let Err(e) = crate::commands::env::global_install::install(package, None, false).await { + vite_shared::output::raw_stderr(&format!("Failed to update {package}: {e}")); + return Ok(exit_status(1)); } - _ => false, } -} - -/// Get available tasks for shell completion. -/// -/// Delegates to the local vite-plus CLI to run `vp run` without arguments, -/// which returns a list of available tasks in the format "task_name: description". -fn run_tasks_completions(current: &OsStr) -> Vec { - let Ok(cwd) = vite_path::current_dir() else { - return vec![]; - }; - - // Unescape hashtag and trim quotes for better matching - let current = current - .to_string_lossy() - .replace("\\#", "#") - .trim_matches(|c| c == '"' || c == '\'') - .to_string(); - - let output = tokio::task::block_in_place(|| { - Runtime::new().ok().and_then(|rt| { - rt.block_on(async { commands::delegate::execute_output(cwd, "run", &[]).await.ok() }) - }) - }); - - output - .filter(|o| o.status.success()) - .map(|output| { - String::from_utf8_lossy(&output.stdout) - .lines() - .filter_map(|line| line.split_once(": ").map(|(name, _)| name.trim())) - .filter(|name| !name.is_empty()) - .filter(|name| name.starts_with(¤t) || current.is_empty()) - .map(|name| clap_complete::CompletionCandidate::new(name.to_string())) - .collect() - }) - .unwrap_or_default() + Ok(ExitStatus::default()) } /// Run the CLI command. @@ -1619,344 +626,17 @@ pub async fn run_command_with_options( match command { // Category A: Package Manager Commands - Commands::Install { - prod, - dev, - no_optional, - frozen_lockfile, - no_frozen_lockfile, - lockfile_only, - prefer_offline, - offline, - force, - ignore_scripts, - no_lockfile, - fix_lockfile, - shamefully_hoist, - resolution_only, - silent, - filter, - workspace_root, - save_exact, - save_peer, - save_optional, - save_catalog, - global, - node, - packages, - pass_through_args, - } => { - print_runtime_header(render_options.show_header && !silent); - // If packages are provided, redirect to Add command - if let Some(pkgs) = packages - && !pkgs.is_empty() - { - // Handle global install via vite-plus managed global install - if global { - use crate::commands::env::global_install; - for package in &pkgs { - if let Err(e) = - global_install::install(package, node.as_deref(), force).await - { - eprintln!("Failed to install {}: {}", package, e); - return Ok(exit_status(1)); - } - } - return Ok(ExitStatus::default()); - } - - let save_dependency_type = - determine_save_dependency_type(dev, save_peer, save_optional, prod); - - return AddCommand::new(cwd) - .execute( - &pkgs, - save_dependency_type, - save_exact, - if save_catalog { Some("default") } else { None }, - filter.as_deref(), - workspace_root, - false, // workspace_only - global, - None, // allow_build - pass_through_args.as_deref(), - ) - .await; - } - - // No packages provided, run regular install - let options = InstallCommandOptions { - prod, - dev, - no_optional, - frozen_lockfile, - no_frozen_lockfile, - lockfile_only, - prefer_offline, - offline, - force, - ignore_scripts, - no_lockfile, - fix_lockfile, - shamefully_hoist, - resolution_only, - silent, - filters: filter.as_deref(), - workspace_root, - pass_through_args: pass_through_args.as_deref(), - }; - InstallCommand::new(cwd).execute(&options).await - } - - Commands::Add { - save_prod, - save_dev, - save_peer, - save_optional, - save_exact, - save_catalog_name, - save_catalog, - allow_build, - filter, - workspace_root, - workspace, - global, - node, - packages, - pass_through_args, - } => { - // Handle global install via vite-plus managed global install - if global { - use crate::commands::env::global_install; - for package in &packages { - if let Err(e) = global_install::install(package, node.as_deref(), false).await { - eprintln!("Failed to install {}: {}", package, e); - return Ok(exit_status(1)); - } - } - return Ok(ExitStatus::default()); - } - - let save_dependency_type = - determine_save_dependency_type(save_dev, save_peer, save_optional, save_prod); - - let catalog_name = - if save_catalog { Some("default") } else { save_catalog_name.as_deref() }; - - AddCommand::new(cwd) - .execute( - &packages, - save_dependency_type, - save_exact, - catalog_name, - filter.as_deref(), - workspace_root, - workspace, - global, - allow_build.as_deref(), - pass_through_args.as_deref(), - ) - .await - } - - Commands::Remove { - save_dev, - save_optional, - save_prod, - filter, - workspace_root, - recursive, - global, - dry_run, - packages, - pass_through_args, - } => { - // Handle global uninstall via vite-plus managed global install - if global { - use crate::commands::env::global_install; - for package in &packages { - if let Err(e) = global_install::uninstall(package, dry_run).await { - eprintln!("Failed to uninstall {}: {}", package, e); - return Ok(exit_status(1)); - } - } - return Ok(ExitStatus::default()); - } - - RemoveCommand::new(cwd) - .execute( - &packages, - save_dev, - save_optional, - save_prod, - filter.as_deref(), - workspace_root, - recursive, - global, - pass_through_args.as_deref(), - ) - .await - } - - Commands::Update { - latest, - global, - recursive, - filter, - workspace_root, - dev, - prod, - interactive, - no_optional, - no_save, - workspace, - packages, - pass_through_args, - } => { - // Handle global update via vite-plus managed global install - if global { - use crate::commands::env::{global_install, package_metadata::PackageMetadata}; - - let packages_to_update = if packages.is_empty() { - let all = PackageMetadata::list_all().await?; - if all.is_empty() { - println!("No global packages installed."); - return Ok(ExitStatus::default()); - } - all.iter().map(|p| p.name.clone()).collect::>() - } else { - packages.clone() - }; - for package in &packages_to_update { - if let Err(e) = global_install::install(package, None, false).await { - eprintln!("Failed to update {}: {}", package, e); - return Ok(exit_status(1)); - } - } - return Ok(ExitStatus::default()); + // Print the runtime header for `vp install` (when not silent). + // Then intercept any `--global` paths that need vite-plus-managed + // global install, falling through to `vite_pm_cli::dispatch` for + // every project-scoped PM operation. + Commands::PackageManager(pm_command) => { + if let PackageManagerCommand::Install { silent, .. } = &pm_command { + print_runtime_header(render_options.show_header && !*silent); } - - UpdateCommand::new(cwd) - .execute( - &packages, - latest, - recursive, - filter.as_deref(), - workspace_root, - dev, - prod, - interactive, - no_optional, - no_save, - workspace, - pass_through_args.as_deref(), - ) - .await - } - - Commands::Dedupe { check, pass_through_args } => { - DedupeCommand::new(cwd).execute(check, pass_through_args.as_deref()).await - } - - Commands::Outdated { - packages, - long, - format, - recursive, - filter, - workspace_root, - prod, - dev, - no_optional, - compatible, - sort_by, - global, - pass_through_args, - } => { - OutdatedCommand::new(cwd) - .execute( - &packages, - long, - format, - recursive, - filter.as_deref(), - workspace_root, - prod, - dev, - no_optional, - compatible, - sort_by.as_deref(), - global, - pass_through_args.as_deref(), - ) - .await - } - - Commands::Why { - packages, - json, - long, - parseable, - recursive, - filter, - workspace_root, - prod, - dev, - depth, - no_optional, - global, - exclude_peers, - find_by, - pass_through_args, - } => { - WhyCommand::new(cwd) - .execute( - &packages, - json, - long, - parseable, - recursive, - filter.as_deref(), - workspace_root, - prod, - dev, - depth, - no_optional, - global, - exclude_peers, - find_by.as_deref(), - pass_through_args.as_deref(), - ) - .await + run_package_manager_command(cwd, pm_command).await } - Commands::Info { package, field, json, pass_through_args } => { - commands::pm::execute_info( - cwd, - &package, - field.as_deref(), - json, - pass_through_args.as_deref(), - ) - .await - } - - Commands::Link { package, args } => { - let pass_through = if args.is_empty() { None } else { Some(args.as_slice()) }; - LinkCommand::new(cwd).execute(package.as_deref(), pass_through).await - } - - Commands::Unlink { package, recursive, args } => { - let pass_through = if args.is_empty() { None } else { Some(args.as_slice()) }; - UnlinkCommand::new(cwd).execute(package.as_deref(), recursive, pass_through).await - } - - Commands::Dlx { package, shell_mode, silent, args } => { - DlxCommand::new(cwd).execute(package, shell_mode, silent, args).await - } - - Commands::Pm(pm_command) => commands::pm::execute_pm_subcommand(cwd, pm_command).await, - // Category B: JS Script Commands Commands::Create { args } => commands::create::execute(cwd, &args).await, diff --git a/crates/vite_global_cli/src/commands/add.rs b/crates/vite_global_cli/src/commands/add.rs deleted file mode 100644 index 30b51cb1f0..0000000000 --- a/crates/vite_global_cli/src/commands/add.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::process::ExitStatus; - -use vite_install::{ - commands::add::{AddCommandOptions, SaveDependencyType}, - package_manager::PackageManager, -}; -use vite_path::AbsolutePathBuf; - -use super::prepend_js_runtime_to_path_env; -use crate::error::Error; - -/// Add command for adding packages to dependencies. -/// -/// This command automatically detects the package manager and translates -/// the add command to the appropriate package manager-specific syntax. -pub struct AddCommand { - cwd: AbsolutePathBuf, -} - -impl AddCommand { - pub fn new(cwd: AbsolutePathBuf) -> Self { - Self { cwd } - } - - pub async fn execute( - self, - packages: &[String], - save_dependency_type: Option, - save_exact: bool, - save_catalog_name: Option<&str>, - filters: Option<&[String]>, - workspace_root: bool, - workspace_only: bool, - global: bool, - allow_build: Option<&str>, - pass_through_args: Option<&[String]>, - ) -> Result { - prepend_js_runtime_to_path_env(&self.cwd).await?; - super::ensure_package_json(&self.cwd).await?; - - let add_command_options = AddCommandOptions { - packages, - save_dependency_type, - save_exact, - filters, - workspace_root, - workspace_only, - global, - save_catalog_name, - allow_build, - pass_through_args, - }; - - // Detect package manager - let package_manager = PackageManager::builder(&self.cwd).build_with_default().await?; - - Ok(package_manager.run_add_command(&add_command_options, &self.cwd).await?) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_add_command_new() { - let workspace_root = if cfg!(windows) { - AbsolutePathBuf::new("C:\\test".into()).unwrap() - } else { - AbsolutePathBuf::new("/test".into()).unwrap() - }; - - let cmd = AddCommand::new(workspace_root.clone()); - assert_eq!(cmd.cwd, workspace_root); - } -} diff --git a/crates/vite_global_cli/src/commands/dedupe.rs b/crates/vite_global_cli/src/commands/dedupe.rs deleted file mode 100644 index d4187c4425..0000000000 --- a/crates/vite_global_cli/src/commands/dedupe.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::process::ExitStatus; - -use vite_install::commands::dedupe::DedupeCommandOptions; -use vite_path::AbsolutePathBuf; - -use super::{build_package_manager, prepend_js_runtime_to_path_env}; -use crate::error::Error; - -/// Dedupe command for deduplicating dependencies by removing older versions. -/// -/// This command automatically detects the package manager and translates -/// the dedupe command to the appropriate package manager-specific syntax. -pub struct DedupeCommand { - cwd: AbsolutePathBuf, -} - -impl DedupeCommand { - pub fn new(cwd: AbsolutePathBuf) -> Self { - Self { cwd } - } - - pub async fn execute( - self, - check: bool, - pass_through_args: Option<&[String]>, - ) -> Result { - prepend_js_runtime_to_path_env(&self.cwd).await?; - - let package_manager = build_package_manager(&self.cwd).await?; - - let dedupe_command_options = DedupeCommandOptions { check, pass_through_args }; - Ok(package_manager.run_dedupe_command(&dedupe_command_options, &self.cwd).await?) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_dedupe_command_new() { - let workspace_root = if cfg!(windows) { - AbsolutePathBuf::new("C:\\test".into()).unwrap() - } else { - AbsolutePathBuf::new("/test".into()).unwrap() - }; - - let cmd = DedupeCommand::new(workspace_root.clone()); - assert_eq!(cmd.cwd, workspace_root); - } -} diff --git a/crates/vite_global_cli/src/commands/dlx.rs b/crates/vite_global_cli/src/commands/dlx.rs deleted file mode 100644 index 8540808d59..0000000000 --- a/crates/vite_global_cli/src/commands/dlx.rs +++ /dev/null @@ -1,87 +0,0 @@ -use std::{collections::HashMap, process::ExitStatus}; - -use vite_command::run_command; -use vite_install::{ - commands::dlx::{DlxCommandOptions, build_npx_args}, - package_manager::PackageManager, -}; -use vite_path::AbsolutePathBuf; - -use super::prepend_js_runtime_to_path_env; -use crate::error::Error; - -/// Dlx command for executing packages without installing them as dependencies. -/// -/// This command automatically detects the package manager and translates -/// the dlx command to the appropriate package manager-specific syntax: -/// - pnpm: pnpm dlx -/// - npm: npm exec -/// - yarn@2+: yarn dlx -/// - yarn@1: falls back to npx -/// -/// When no package.json is found, falls back to npx directly. -pub struct DlxCommand { - cwd: AbsolutePathBuf, -} - -impl DlxCommand { - pub fn new(cwd: AbsolutePathBuf) -> Self { - Self { cwd } - } - - pub async fn execute( - self, - packages: Vec, - shell_mode: bool, - silent: bool, - args: Vec, - ) -> Result { - if args.is_empty() { - return Err(Error::Other("dlx requires a package name".into())); - } - - prepend_js_runtime_to_path_env(&self.cwd).await?; - - // First arg is the package spec, rest are command args - let package_spec = &args[0]; - let command_args: Vec = args[1..].to_vec(); - - let dlx_command_options = DlxCommandOptions { - packages: &packages, - package_spec, - args: &command_args, - shell_mode, - silent, - }; - - match PackageManager::builder(&self.cwd).build_with_default().await { - Ok(pm) => Ok(pm.run_dlx_command(&dlx_command_options, &self.cwd).await?), - Err(vite_error::Error::WorkspaceError(vite_workspace::Error::PackageJsonNotFound( - _, - ))) => { - // No package.json found — fall back to npx directly - let args = build_npx_args(&dlx_command_options); - let envs = HashMap::new(); - Ok(run_command("npx", &args, &envs, &self.cwd).await?) - } - Err(e) => Err(e.into()), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_dlx_command_new() { - let workspace_root = if cfg!(windows) { - AbsolutePathBuf::new("C:\\test".into()).unwrap() - } else { - AbsolutePathBuf::new("/test".into()).unwrap() - }; - - let cmd = DlxCommand::new(workspace_root.clone()); - assert_eq!(cmd.cwd, workspace_root); - } -} diff --git a/crates/vite_global_cli/src/commands/install.rs b/crates/vite_global_cli/src/commands/install.rs deleted file mode 100644 index 1a52c1e055..0000000000 --- a/crates/vite_global_cli/src/commands/install.rs +++ /dev/null @@ -1,133 +0,0 @@ -use std::process::ExitStatus; - -use vite_install::{PackageManager, commands::install::InstallCommandOptions}; -use vite_path::AbsolutePathBuf; - -use super::prepend_js_runtime_to_path_env; -use crate::error::Error; - -/// Install command. -pub struct InstallCommand { - cwd: AbsolutePathBuf, -} - -impl InstallCommand { - pub fn new(cwd: AbsolutePathBuf) -> Self { - Self { cwd } - } - - pub async fn execute(self, options: &InstallCommandOptions<'_>) -> Result { - prepend_js_runtime_to_path_env(&self.cwd).await?; - super::ensure_package_json(&self.cwd).await?; - - let package_manager = PackageManager::builder(&self.cwd).build_with_default().await?; - - Ok(package_manager.run_install_command(options, &self.cwd).await?) - } -} - -#[cfg(test)] -mod tests { - use std::{fs, path::PathBuf}; - - use tempfile::TempDir; - - use super::*; - - #[test] - fn test_install_command_new() { - let workspace_root = AbsolutePathBuf::new(PathBuf::from(if cfg!(windows) { - "C:\\test\\workspace" - } else { - "/test/workspace" - })) - .unwrap(); - let command = InstallCommand::new(workspace_root.clone()); - - assert_eq!(command.cwd, workspace_root); - } - - #[ignore = "skip this test for auto run, should be run manually, because it will prompt for user selection"] - #[tokio::test] - async fn test_install_command_with_package_json_without_package_manager() { - let temp_dir = TempDir::new().unwrap(); - let workspace_root = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - - // Create a minimal package.json - let package_json = r#"{ - "name": "test-package", - "version": "1.0.0" - }"#; - fs::write(workspace_root.join("package.json"), package_json).unwrap(); - - let command = InstallCommand::new(workspace_root); - assert!(command.execute(&InstallCommandOptions::default()).await.is_ok()); - } - - #[tokio::test] - #[serial_test::serial] - async fn test_install_command_with_package_json_with_package_manager() { - let temp_dir = TempDir::new().unwrap(); - let workspace_root = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - - // Create a minimal package.json - let package_json = r#"{ - "name": "test-package", - "version": "1.0.0", - "packageManager": "pnpm@10.15.0" - }"#; - fs::write(workspace_root.join("package.json"), package_json).unwrap(); - - let command = InstallCommand::new(workspace_root); - let result = command.execute(&InstallCommandOptions::default()).await; - println!("result: {result:?}"); - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_ensure_package_json_creates_when_missing() { - let temp_dir = TempDir::new().unwrap(); - let dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_json_path = dir_path.join("package.json"); - - // Verify no package.json exists - assert!(!package_json_path.as_path().exists()); - - // Call ensure_package_json - crate::commands::ensure_package_json(&dir_path).await.unwrap(); - - // Verify package.json was created with correct content - let content = fs::read_to_string(&package_json_path).unwrap(); - let parsed: serde_json::Value = serde_json::from_str(&content).unwrap(); - assert_eq!(parsed["type"], "module"); - } - - #[tokio::test] - async fn test_ensure_package_json_does_not_overwrite_existing() { - let temp_dir = TempDir::new().unwrap(); - let dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); - let package_json_path = dir_path.join("package.json"); - - // Create an existing package.json - let existing_content = r#"{"name": "existing-package"}"#; - fs::write(&package_json_path, existing_content).unwrap(); - - // Call ensure_package_json - crate::commands::ensure_package_json(&dir_path).await.unwrap(); - - // Verify existing package.json was NOT overwritten - let content = fs::read_to_string(&package_json_path).unwrap(); - assert_eq!(content, existing_content); - } - - #[tokio::test] - async fn test_install_command_execute_with_invalid_workspace() { - let temp_dir = TempDir::new().unwrap(); - let workspace_root = AbsolutePathBuf::new(temp_dir.path().join("nonexistent")).unwrap(); - - let command = InstallCommand::new(workspace_root); - - let result = command.execute(&InstallCommandOptions::default()).await; - assert!(result.is_err()); - } -} diff --git a/crates/vite_global_cli/src/commands/link.rs b/crates/vite_global_cli/src/commands/link.rs deleted file mode 100644 index 3356ee4a65..0000000000 --- a/crates/vite_global_cli/src/commands/link.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::process::ExitStatus; - -use vite_install::commands::link::LinkCommandOptions; -use vite_path::AbsolutePathBuf; - -use super::{build_package_manager, prepend_js_runtime_to_path_env}; -use crate::error::Error; - -/// Link command for local package development. -/// -/// This command automatically detects the package manager and translates -/// the link command to the appropriate package manager-specific syntax. -pub struct LinkCommand { - cwd: AbsolutePathBuf, -} - -impl LinkCommand { - pub fn new(cwd: AbsolutePathBuf) -> Self { - Self { cwd } - } - - pub async fn execute( - self, - package: Option<&str>, - pass_through_args: Option<&[String]>, - ) -> Result { - prepend_js_runtime_to_path_env(&self.cwd).await?; - - let package_manager = build_package_manager(&self.cwd).await?; - - let link_command_options = LinkCommandOptions { package, pass_through_args }; - Ok(package_manager.run_link_command(&link_command_options, &self.cwd).await?) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_link_command_new() { - let workspace_root = if cfg!(windows) { - AbsolutePathBuf::new("C:\\test".into()).unwrap() - } else { - AbsolutePathBuf::new("/test".into()).unwrap() - }; - - let cmd = LinkCommand::new(workspace_root.clone()); - assert_eq!(cmd.cwd, workspace_root); - } -} diff --git a/crates/vite_global_cli/src/commands/mod.rs b/crates/vite_global_cli/src/commands/mod.rs index 7d0f45a839..c51ca94051 100644 --- a/crates/vite_global_cli/src/commands/mod.rs +++ b/crates/vite_global_cli/src/commands/mod.rs @@ -2,18 +2,11 @@ //! //! Commands are organized by category: //! -//! Category A - Package manager commands: -//! - `add`: Add packages to dependencies -//! - `install`: Install all dependencies -//! - `remove`: Remove packages from dependencies -//! - `update`: Update packages to their latest versions -//! - `dedupe`: Deduplicate dependencies -//! - `outdated`: Check for outdated packages -//! - `why`: Show why a package is installed -//! - `link`: Link packages for local development -//! - `unlink`: Unlink packages -//! - `dlx`: Execute a package binary without installing it -//! - `pm`: Forward commands to the package manager +//! Category A - Package manager commands (clap defs and dispatch live in +//! the shared `vite_pm_cli` crate; the global CLI's `cli.rs` only adds +//! the `--global` interception layer for vite-plus-managed installs): +//! - `add`, `install`, `remove`, `update`, `dedupe`, `outdated`, `why`, +//! `info`, `link`, `unlink`, `dlx`, `pm ` //! //! Category B - JS Script Commands: //! - `create`: Project scaffolding @@ -25,7 +18,6 @@ use std::{collections::HashMap, io::BufReader}; -use vite_install::package_manager::{PackageManager, PackageManagerType}; use vite_path::AbsolutePath; use vite_shared::{PrependOptions, prepend_to_path_env}; @@ -67,20 +59,6 @@ pub fn has_vite_plus_dependency(cwd: &AbsolutePath) -> bool { } } -/// Ensure a package.json exists in the given directory. -/// If it doesn't exist, create a minimal one with `{ "type": "module" }`. -pub async fn ensure_package_json(project_path: &AbsolutePath) -> Result<(), Error> { - let package_json_path = project_path.join("package.json"); - if !package_json_path.as_path().exists() { - let content = serde_json::to_string_pretty(&serde_json::json!({ - "type": "module" - }))?; - tokio::fs::write(&package_json_path, format!("{content}\n")).await?; - tracing::info!("Created package.json in {:?}", project_path); - } - Ok(()) -} - /// Ensure the JS runtime is downloaded and prepend its bin directory to PATH. /// This should be called before executing any package manager command. /// @@ -107,61 +85,6 @@ pub async fn prepend_js_runtime_to_path_env(project_path: &AbsolutePath) -> Resu Ok(()) } -/// Build a PackageManager, converting PackageJsonNotFound into a friendly error message. -pub async fn build_package_manager(cwd: &AbsolutePath) -> Result { - match PackageManager::builder(cwd).build_with_default().await { - Ok(pm) => Ok(pm), - Err(vite_error::Error::WorkspaceError(vite_workspace::Error::PackageJsonNotFound(_))) => { - Err(Error::UserMessage("No package.json found.".into())) - } - Err(e) => Err(e.into()), - } -} - -/// Build a PackageManager, falling back to a default npm instance when no -/// package.json is found. Uses `build()` instead of `build_with_default()` -/// to skip the interactive package manager selection prompt on the fallback path. -/// -/// Requires `prepend_js_runtime_to_path_env` to be called first so npm is on PATH. -pub async fn build_package_manager_or_npm_default( - cwd: &AbsolutePath, -) -> Result { - match PackageManager::builder(cwd).build().await { - Ok(pm) => Ok(pm), - Err(vite_error::Error::WorkspaceError(vite_workspace::Error::PackageJsonNotFound(_))) - | Err(vite_error::Error::UnrecognizedPackageManager) => { - Ok(default_npm_package_manager(cwd)) - } - Err(e) => Err(e.into()), - } -} - -fn default_npm_package_manager(cwd: &AbsolutePath) -> PackageManager { - PackageManager { - client: PackageManagerType::Npm, - package_name: "npm".into(), - version: "latest".into(), - hash: None, - bin_name: "npm".into(), - workspace_root: cwd.to_absolute_path_buf(), - is_monorepo: false, - install_dir: cwd.to_absolute_path_buf(), - } -} - -// Category A: Package manager commands -pub mod add; -pub mod dedupe; -pub mod dlx; -pub mod install; -pub mod link; -pub mod outdated; -pub mod pm; -pub mod remove; -pub mod unlink; -pub mod update; -pub mod why; - // Category B: JS Script Commands pub mod config; pub mod create; @@ -183,18 +106,6 @@ pub mod upgrade; // Category C: Local CLI Delegation pub mod delegate; -// Re-export command structs for convenient access -pub use add::AddCommand; -pub use dedupe::DedupeCommand; -pub use dlx::DlxCommand; -pub use install::InstallCommand; -pub use link::LinkCommand; -pub use outdated::OutdatedCommand; -pub use remove::RemoveCommand; -pub use unlink::UnlinkCommand; -pub use update::UpdateCommand; -pub use why::WhyCommand; - #[cfg(test)] mod tests { use vite_path::AbsolutePathBuf; diff --git a/crates/vite_global_cli/src/commands/outdated.rs b/crates/vite_global_cli/src/commands/outdated.rs deleted file mode 100644 index 725e8a1d67..0000000000 --- a/crates/vite_global_cli/src/commands/outdated.rs +++ /dev/null @@ -1,77 +0,0 @@ -use std::process::ExitStatus; - -use vite_install::commands::outdated::{Format, OutdatedCommandOptions}; -use vite_path::AbsolutePathBuf; - -use super::{build_package_manager, prepend_js_runtime_to_path_env}; -use crate::error::Error; - -/// Outdated command for checking outdated packages. -/// -/// This command automatically detects the package manager and translates -/// the outdated command to the appropriate package manager-specific syntax. -pub struct OutdatedCommand { - cwd: AbsolutePathBuf, -} - -impl OutdatedCommand { - pub fn new(cwd: AbsolutePathBuf) -> Self { - Self { cwd } - } - - #[allow(clippy::too_many_arguments)] - pub async fn execute( - self, - packages: &[String], - long: bool, - format: Option, - recursive: bool, - filters: Option<&[String]>, - workspace_root: bool, - prod: bool, - dev: bool, - no_optional: bool, - compatible: bool, - sort_by: Option<&str>, - global: bool, - pass_through_args: Option<&[String]>, - ) -> Result { - prepend_js_runtime_to_path_env(&self.cwd).await?; - - let package_manager = build_package_manager(&self.cwd).await?; - - let outdated_command_options = OutdatedCommandOptions { - packages, - long, - format, - recursive, - filters, - workspace_root, - prod, - dev, - no_optional, - compatible, - sort_by, - global, - pass_through_args, - }; - Ok(package_manager.run_outdated_command(&outdated_command_options, &self.cwd).await?) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_outdated_command_new() { - let workspace_root = if cfg!(windows) { - AbsolutePathBuf::new("C:\\test".into()).unwrap() - } else { - AbsolutePathBuf::new("/test".into()).unwrap() - }; - - let cmd = OutdatedCommand::new(workspace_root.clone()); - assert_eq!(cmd.cwd, workspace_root); - } -} diff --git a/crates/vite_global_cli/src/commands/remove.rs b/crates/vite_global_cli/src/commands/remove.rs deleted file mode 100644 index d1b43e02ce..0000000000 --- a/crates/vite_global_cli/src/commands/remove.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::process::ExitStatus; - -use vite_install::commands::remove::RemoveCommandOptions; -use vite_path::AbsolutePathBuf; - -use super::{build_package_manager, prepend_js_runtime_to_path_env}; -use crate::error::Error; - -/// Remove command for removing packages from dependencies. -/// -/// This command automatically detects the package manager and translates -/// the remove command to the appropriate package manager-specific syntax. -pub struct RemoveCommand { - cwd: AbsolutePathBuf, -} - -impl RemoveCommand { - pub fn new(cwd: AbsolutePathBuf) -> Self { - Self { cwd } - } - - pub async fn execute( - self, - packages: &[String], - save_dev: bool, - save_optional: bool, - save_prod: bool, - filters: Option<&[String]>, - workspace_root: bool, - recursive: bool, - global: bool, - pass_through_args: Option<&[String]>, - ) -> Result { - prepend_js_runtime_to_path_env(&self.cwd).await?; - - let package_manager = build_package_manager(&self.cwd).await?; - - let remove_command_options = RemoveCommandOptions { - packages, - filters, - workspace_root, - recursive, - global, - save_dev, - save_optional, - save_prod, - pass_through_args, - }; - Ok(package_manager.run_remove_command(&remove_command_options, &self.cwd).await?) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_remove_command_new() { - let workspace_root = if cfg!(windows) { - AbsolutePathBuf::new("C:\\test".into()).unwrap() - } else { - AbsolutePathBuf::new("/test".into()).unwrap() - }; - - let cmd = RemoveCommand::new(workspace_root.clone()); - assert_eq!(cmd.cwd, workspace_root); - } -} diff --git a/crates/vite_global_cli/src/commands/unlink.rs b/crates/vite_global_cli/src/commands/unlink.rs deleted file mode 100644 index 1585ce5c3f..0000000000 --- a/crates/vite_global_cli/src/commands/unlink.rs +++ /dev/null @@ -1,52 +0,0 @@ -use std::process::ExitStatus; - -use vite_install::commands::unlink::UnlinkCommandOptions; -use vite_path::AbsolutePathBuf; - -use super::{build_package_manager, prepend_js_runtime_to_path_env}; -use crate::error::Error; - -/// Unlink command for removing package links. -/// -/// This command automatically detects the package manager and translates -/// the unlink command to the appropriate package manager-specific syntax. -pub struct UnlinkCommand { - cwd: AbsolutePathBuf, -} - -impl UnlinkCommand { - pub fn new(cwd: AbsolutePathBuf) -> Self { - Self { cwd } - } - - pub async fn execute( - self, - package: Option<&str>, - recursive: bool, - pass_through_args: Option<&[String]>, - ) -> Result { - prepend_js_runtime_to_path_env(&self.cwd).await?; - - let package_manager = build_package_manager(&self.cwd).await?; - - let unlink_command_options = UnlinkCommandOptions { package, recursive, pass_through_args }; - Ok(package_manager.run_unlink_command(&unlink_command_options, &self.cwd).await?) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_unlink_command_new() { - let workspace_root = if cfg!(windows) { - AbsolutePathBuf::new("C:\\test".into()).unwrap() - } else { - AbsolutePathBuf::new("/test".into()).unwrap() - }; - - let cmd = UnlinkCommand::new(workspace_root.clone()); - assert_eq!(cmd.cwd, workspace_root); - } -} diff --git a/crates/vite_global_cli/src/commands/update.rs b/crates/vite_global_cli/src/commands/update.rs deleted file mode 100644 index 64a9640e10..0000000000 --- a/crates/vite_global_cli/src/commands/update.rs +++ /dev/null @@ -1,75 +0,0 @@ -use std::process::ExitStatus; - -use vite_install::commands::update::UpdateCommandOptions; -use vite_path::AbsolutePathBuf; - -use super::{build_package_manager, prepend_js_runtime_to_path_env}; -use crate::error::Error; - -/// Update command for updating packages to their latest versions. -/// -/// This command automatically detects the package manager and translates -/// the update command to the appropriate package manager-specific syntax. -pub struct UpdateCommand { - cwd: AbsolutePathBuf, -} - -impl UpdateCommand { - pub fn new(cwd: AbsolutePathBuf) -> Self { - Self { cwd } - } - - #[allow(clippy::too_many_arguments)] - pub async fn execute( - self, - packages: &[String], - latest: bool, - recursive: bool, - filters: Option<&[String]>, - workspace_root: bool, - dev: bool, - prod: bool, - interactive: bool, - no_optional: bool, - no_save: bool, - workspace_only: bool, - pass_through_args: Option<&[String]>, - ) -> Result { - prepend_js_runtime_to_path_env(&self.cwd).await?; - - let package_manager = build_package_manager(&self.cwd).await?; - - let update_command_options = UpdateCommandOptions { - packages, - latest, - recursive, - filters, - workspace_root, - dev, - prod, - interactive, - no_optional, - no_save, - workspace_only, - pass_through_args, - }; - Ok(package_manager.run_update_command(&update_command_options, &self.cwd).await?) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_update_command_new() { - let workspace_root = if cfg!(windows) { - AbsolutePathBuf::new("C:\\test".into()).unwrap() - } else { - AbsolutePathBuf::new("/test".into()).unwrap() - }; - - let cmd = UpdateCommand::new(workspace_root.clone()); - assert_eq!(cmd.cwd, workspace_root); - } -} diff --git a/crates/vite_global_cli/src/commands/vpx.rs b/crates/vite_global_cli/src/commands/vpx.rs index 39745265f7..785772db5a 100644 --- a/crates/vite_global_cli/src/commands/vpx.rs +++ b/crates/vite_global_cli/src/commands/vpx.rs @@ -10,7 +10,6 @@ use vite_path::{AbsolutePath, AbsolutePathBuf}; use vite_shared::{PrependOptions, output, prepend_to_path_env}; -use super::DlxCommand; use crate::{commands::env::config, shim::dispatch}; /// Parsed vpx flags. @@ -110,11 +109,17 @@ pub async fn execute_vpx(args: &[String], cwd: &AbsolutePath) -> i32 { } // 4. Fall back to dlx (remote download) - let cwd_buf = cwd.to_absolute_path_buf(); - match DlxCommand::new(cwd_buf) - .execute(flags.packages, flags.shell_mode, flags.silent, positional) - .await - { + if let Err(e) = super::prepend_js_runtime_to_path_env(cwd).await { + output::error(&format!("vpx: {e}")); + return 1; + } + let dlx = vite_pm_cli::PackageManagerCommand::Dlx { + package: flags.packages, + shell_mode: flags.shell_mode, + silent: flags.silent, + args: positional, + }; + match vite_pm_cli::dispatch(cwd, dlx).await { Ok(status) => status.code().unwrap_or(1), Err(e) => { output::error(&format!("vpx: {e}")); diff --git a/crates/vite_global_cli/src/commands/why.rs b/crates/vite_global_cli/src/commands/why.rs deleted file mode 100644 index d15d65b85f..0000000000 --- a/crates/vite_global_cli/src/commands/why.rs +++ /dev/null @@ -1,81 +0,0 @@ -use std::process::ExitStatus; - -use vite_install::commands::why::WhyCommandOptions; -use vite_path::AbsolutePathBuf; - -use super::{build_package_manager, prepend_js_runtime_to_path_env}; -use crate::error::Error; - -/// Why command for showing why a package is installed. -/// -/// This command automatically detects the package manager and translates -/// the why command to the appropriate package manager-specific syntax. -pub struct WhyCommand { - cwd: AbsolutePathBuf, -} - -impl WhyCommand { - pub fn new(cwd: AbsolutePathBuf) -> Self { - Self { cwd } - } - - #[allow(clippy::too_many_arguments)] - pub async fn execute( - self, - packages: &[String], - json: bool, - long: bool, - parseable: bool, - recursive: bool, - filters: Option<&[String]>, - workspace_root: bool, - prod: bool, - dev: bool, - depth: Option, - no_optional: bool, - global: bool, - exclude_peers: bool, - find_by: Option<&str>, - pass_through_args: Option<&[String]>, - ) -> Result { - prepend_js_runtime_to_path_env(&self.cwd).await?; - - let package_manager = build_package_manager(&self.cwd).await?; - - let why_command_options = WhyCommandOptions { - packages, - json, - long, - parseable, - recursive, - filters, - workspace_root, - prod, - dev, - depth, - no_optional, - global, - exclude_peers, - find_by, - pass_through_args, - }; - Ok(package_manager.run_why_command(&why_command_options, &self.cwd).await?) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_why_command_new() { - let workspace_root = if cfg!(windows) { - AbsolutePathBuf::new("C:\\test".into()).unwrap() - } else { - AbsolutePathBuf::new("/test".into()).unwrap() - }; - - let cmd = WhyCommand::new(workspace_root.clone()); - assert_eq!(cmd.cwd, workspace_root); - } -} diff --git a/crates/vite_global_cli/src/error.rs b/crates/vite_global_cli/src/error.rs index 769310d727..0e44a42adf 100644 --- a/crates/vite_global_cli/src/error.rs +++ b/crates/vite_global_cli/src/error.rs @@ -64,4 +64,15 @@ pub enum Error { version_source: String, help: String, }, + + #[error(transparent)] + PmCli(#[from] vite_pm_cli::Error), +} + +impl Error { + /// Whether this error should be printed without the "error: " prefix + /// (a friendly user-facing message, not a stack trace). + pub fn is_user_message(&self) -> bool { + matches!(self, Self::UserMessage(_) | Self::PmCli(vite_pm_cli::Error::UserMessage(_))) + } } diff --git a/crates/vite_global_cli/src/main.rs b/crates/vite_global_cli/src/main.rs index 999c2f2afe..7c0dba8100 100644 --- a/crates/vite_global_cli/src/main.rs +++ b/crates/vite_global_cli/src/main.rs @@ -196,8 +196,8 @@ async fn run_corrected_args(cwd: &vite_path::AbsolutePathBuf, raw_args: &[String match run_command_with_options(cwd.clone(), parsed, render_options).await { Ok(exit_status) => exit_status_to_exit_code(exit_status), Err(e) => { - if matches!(&e, error::Error::UserMessage(_)) { - eprintln!("{e}"); + if e.is_user_message() { + output::raw_stderr(&format!("{e}")); } else { output::error(&format!("{e}")); } @@ -395,8 +395,8 @@ async fn main() -> ExitCode { Ok(args) => match run_command(cwd.clone(), args).await { Ok(exit_status) => exit_status_to_exit_code(exit_status), Err(e) => { - if matches!(&e, error::Error::UserMessage(_)) { - eprintln!("{e}"); + if e.is_user_message() { + output::raw_stderr(&format!("{e}")); } else { output::error(&format!("{e}")); } diff --git a/crates/vite_pm_cli/Cargo.toml b/crates/vite_pm_cli/Cargo.toml new file mode 100644 index 0000000000..b36d374f4a --- /dev/null +++ b/crates/vite_pm_cli/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "vite_pm_cli" +version = "0.0.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +publish = false +rust-version.workspace = true + +[dependencies] +clap = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["fs", "process", "rt", "macros"] } +tracing = { workspace = true } +vite_command = { workspace = true } +vite_error = { workspace = true } +vite_install = { workspace = true } +vite_path = { workspace = true } +vite_str = { workspace = true } +vite_workspace = { workspace = true } + +[lints] +workspace = true diff --git a/crates/vite_pm_cli/src/cli.rs b/crates/vite_pm_cli/src/cli.rs new file mode 100644 index 0000000000..024a5c92bc --- /dev/null +++ b/crates/vite_pm_cli/src/cli.rs @@ -0,0 +1,1117 @@ +//! Clap definitions for every package-manager subcommand. +//! +//! The top-level [`PackageManagerCommand`] enum is consumed by both the +//! global CLI and the local CLI binding via `#[command(flatten)]`, so any +//! flag added here appears identically in both surfaces. + +use clap::Subcommand; +use vite_install::commands::{add::SaveDependencyType, outdated::Format}; + +/// All package-manager subcommands. +/// +/// Variants intentionally mirror the original definitions in +/// `vite_global_cli/src/cli.rs`. Aliases (`i`, `up`, `rm`, `un`, `uninstall`, +/// `explain`, `view`, `show`, `ln`) are preserved so both CLIs accept the +/// same shorthands. +#[derive(Subcommand, Debug, Clone)] +pub enum PackageManagerCommand { + /// Install all dependencies, or add packages if package names are provided + #[command(visible_alias = "i")] + Install { + /// Do not install devDependencies + #[arg(short = 'P', long)] + prod: bool, + + /// Only install devDependencies (install) / Save to devDependencies (add) + #[arg(short = 'D', long)] + dev: bool, + + /// Do not install optionalDependencies + #[arg(long)] + no_optional: bool, + + /// Fail if lockfile needs to be updated (CI mode) + #[arg(long, overrides_with = "no_frozen_lockfile")] + frozen_lockfile: bool, + + /// Allow lockfile updates (opposite of --frozen-lockfile) + #[arg(long, overrides_with = "frozen_lockfile")] + no_frozen_lockfile: bool, + + /// Only update lockfile, don't install + #[arg(long)] + lockfile_only: bool, + + /// Use cached packages when available + #[arg(long)] + prefer_offline: bool, + + /// Only use packages already in cache + #[arg(long)] + offline: bool, + + /// Force reinstall all dependencies + #[arg(short = 'f', long)] + force: bool, + + /// Do not run lifecycle scripts + #[arg(long)] + ignore_scripts: bool, + + /// Don't read or generate lockfile + #[arg(long)] + no_lockfile: bool, + + /// Fix broken lockfile entries (pnpm and yarn@2+ only) + #[arg(long)] + fix_lockfile: bool, + + /// Create flat `node_modules` (pnpm only) + #[arg(long)] + shamefully_hoist: bool, + + /// Re-run resolution for peer dependency analysis (pnpm only) + #[arg(long)] + resolution_only: bool, + + /// Suppress output (silent mode) + #[arg(long)] + silent: bool, + + /// Filter packages in monorepo (can be used multiple times) + #[arg(long, value_name = "PATTERN")] + filter: Option>, + + /// Install in workspace root only + #[arg(short = 'w', long)] + workspace_root: bool, + + /// Save exact version (only when adding packages) + #[arg(short = 'E', long)] + save_exact: bool, + + /// Save to peerDependencies (only when adding packages) + #[arg(long)] + save_peer: bool, + + /// Save to optionalDependencies (only when adding packages) + #[arg(short = 'O', long)] + save_optional: bool, + + /// Save the new dependency to the default catalog (only when adding packages) + #[arg(long)] + save_catalog: bool, + + /// Install globally (requires package names) + #[arg(short = 'g', long, requires = "packages")] + global: bool, + + /// Node.js version to use for global installation (only with -g) + #[arg(long, requires = "global")] + node: Option, + + /// Packages to add (if provided, acts as `vp add`) + #[arg(required = false)] + packages: Option>, + + /// Additional arguments to pass through to the package manager + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// Add packages to dependencies + Add { + /// Save to `dependencies` (default) + #[arg(short = 'P', long)] + save_prod: bool, + + /// Save to `devDependencies` + #[arg(short = 'D', long)] + save_dev: bool, + + /// Save to `peerDependencies` and `devDependencies` + #[arg(long)] + save_peer: bool, + + /// Save to `optionalDependencies` + #[arg(short = 'O', long)] + save_optional: bool, + + /// Save exact version rather than semver range + #[arg(short = 'E', long)] + save_exact: bool, + + /// Save the new dependency to the specified catalog name + #[arg(long, value_name = "CATALOG_NAME")] + save_catalog_name: Option, + + /// Save the new dependency to the default catalog + #[arg(long)] + save_catalog: bool, + + /// A list of package names allowed to run postinstall + #[arg(long, value_name = "NAMES")] + allow_build: Option, + + /// Filter packages in monorepo (can be used multiple times) + #[arg(long, value_name = "PATTERN")] + filter: Option>, + + /// Add to workspace root + #[arg(short = 'w', long)] + workspace_root: bool, + + /// Only add if package exists in workspace (pnpm-specific) + #[arg(long)] + workspace: bool, + + /// Install globally + #[arg(short = 'g', long)] + global: bool, + + /// Node.js version to use for global installation (only with -g) + #[arg(long, requires = "global")] + node: Option, + + /// Packages to add + #[arg(required = true)] + packages: Vec, + + /// Additional arguments to pass through to the package manager + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// Remove packages from dependencies + #[command(visible_alias = "rm", visible_alias = "un", visible_alias = "uninstall")] + Remove { + /// Only remove from `devDependencies` (pnpm-specific) + #[arg(short = 'D', long)] + save_dev: bool, + + /// Only remove from `optionalDependencies` (pnpm-specific) + #[arg(short = 'O', long)] + save_optional: bool, + + /// Only remove from `dependencies` (pnpm-specific) + #[arg(short = 'P', long)] + save_prod: bool, + + /// Filter packages in monorepo (can be used multiple times) + #[arg(long, value_name = "PATTERN")] + filter: Option>, + + /// Remove from workspace root + #[arg(short = 'w', long)] + workspace_root: bool, + + /// Remove recursively from all workspace packages + #[arg(short = 'r', long)] + recursive: bool, + + /// Remove global packages + #[arg(short = 'g', long)] + global: bool, + + /// Preview what would be removed without actually removing (only with -g) + #[arg(long, requires = "global")] + dry_run: bool, + + /// Packages to remove + #[arg(required = true)] + packages: Vec, + + /// Additional arguments to pass through to the package manager + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// Update packages to their latest versions + #[command(visible_alias = "up")] + Update { + /// Update to latest version (ignore semver range) + #[arg(short = 'L', long)] + latest: bool, + + /// Update global packages + #[arg(short = 'g', long)] + global: bool, + + /// Update recursively in all workspace packages + #[arg(short = 'r', long)] + recursive: bool, + + /// Filter packages in monorepo (can be used multiple times) + #[arg(long, value_name = "PATTERN")] + filter: Option>, + + /// Include workspace root + #[arg(short = 'w', long)] + workspace_root: bool, + + /// Update only devDependencies + #[arg(short = 'D', long)] + dev: bool, + + /// Update only dependencies (production) + #[arg(short = 'P', long)] + prod: bool, + + /// Interactive mode + #[arg(short = 'i', long)] + interactive: bool, + + /// Don't update optionalDependencies + #[arg(long)] + no_optional: bool, + + /// Update lockfile only, don't modify package.json + #[arg(long)] + no_save: bool, + + /// Only update if package exists in workspace (pnpm-specific) + #[arg(long)] + workspace: bool, + + /// Packages to update (optional - updates all if omitted) + packages: Vec, + + /// Additional arguments to pass through to the package manager + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// Deduplicate dependencies + Dedupe { + /// Check if deduplication would make changes + #[arg(long)] + check: bool, + + /// Additional arguments to pass through to the package manager + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// Check for outdated packages + Outdated { + /// Package name(s) to check + packages: Vec, + + /// Show extended information + #[arg(long)] + long: bool, + + /// Output format: table (default), list, or json + #[arg(long, value_name = "FORMAT", value_parser = clap::value_parser!(Format))] + format: Option, + + /// Check recursively across all workspaces + #[arg(short = 'r', long)] + recursive: bool, + + /// Filter packages in monorepo + #[arg(long, value_name = "PATTERN")] + filter: Option>, + + /// Include workspace root + #[arg(short = 'w', long)] + workspace_root: bool, + + /// Only production and optional dependencies + #[arg(short = 'P', long)] + prod: bool, + + /// Only dev dependencies + #[arg(short = 'D', long)] + dev: bool, + + /// Exclude optional dependencies + #[arg(long)] + no_optional: bool, + + /// Only show compatible versions + #[arg(long)] + compatible: bool, + + /// Sort results by field + #[arg(long, value_name = "FIELD")] + sort_by: Option, + + /// Check globally installed packages + #[arg(short = 'g', long)] + global: bool, + + /// Additional arguments to pass through to the package manager + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// Show why a package is installed + #[command(visible_alias = "explain")] + Why { + /// Package(s) to check + #[arg(required = true)] + packages: Vec, + + /// Output in JSON format + #[arg(long)] + json: bool, + + /// Show extended information + #[arg(long)] + long: bool, + + /// Show parseable output + #[arg(long)] + parseable: bool, + + /// Check recursively across all workspaces + #[arg(short = 'r', long)] + recursive: bool, + + /// Filter packages in monorepo + #[arg(long, value_name = "PATTERN")] + filter: Option>, + + /// Check in workspace root + #[arg(short = 'w', long)] + workspace_root: bool, + + /// Only production dependencies + #[arg(short = 'P', long)] + prod: bool, + + /// Only dev dependencies + #[arg(short = 'D', long)] + dev: bool, + + /// Limit tree depth + #[arg(long)] + depth: Option, + + /// Exclude optional dependencies + #[arg(long)] + no_optional: bool, + + /// Check globally installed packages + #[arg(short = 'g', long)] + global: bool, + + /// Exclude peer dependencies + #[arg(long)] + exclude_peers: bool, + + /// Use a finder function defined in .pnpmfile.cjs + #[arg(long, value_name = "FINDER_NAME")] + find_by: Option, + + /// Additional arguments to pass through to the package manager + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// View package information from the registry + #[command(visible_alias = "view", visible_alias = "show")] + Info { + /// Package name with optional version + #[arg(required = true)] + package: String, + + /// Specific field to view + field: Option, + + /// Output in JSON format + #[arg(long)] + json: bool, + + /// Additional arguments to pass through to the package manager + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// Link packages for local development + #[command(visible_alias = "ln")] + Link { + /// Package name or directory to link + #[arg(value_name = "PACKAGE|DIR")] + package: Option, + + /// Arguments to pass to package manager + #[arg(allow_hyphen_values = true, trailing_var_arg = true)] + args: Vec, + }, + + /// Unlink packages + Unlink { + /// Package name to unlink + #[arg(value_name = "PACKAGE|DIR")] + package: Option, + + /// Unlink in every workspace package + #[arg(short = 'r', long)] + recursive: bool, + + /// Arguments to pass to package manager + #[arg(allow_hyphen_values = true, trailing_var_arg = true)] + args: Vec, + }, + + /// Execute a package binary without installing it + Dlx { + /// Package(s) to install before running + #[arg(long, short = 'p', value_name = "NAME")] + package: Vec, + + /// Execute within a shell environment + #[arg(long = "shell-mode", short = 'c')] + shell_mode: bool, + + /// Suppress all output except the executed command's output + #[arg(long, short = 's')] + silent: bool, + + /// Package to execute and arguments + #[arg(required = true, trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, + }, + + /// Forward a command to the package manager + #[command(subcommand)] + Pm(PmCommands), +} + +impl PackageManagerCommand { + /// Whether the command was invoked with flags that request quiet or + /// machine-readable output. + pub fn is_quiet_or_machine_readable(&self) -> bool { + match self { + Self::Install { silent, .. } | Self::Dlx { silent, .. } => *silent, + Self::Outdated { format, .. } => matches!(format, Some(Format::Json | Format::List)), + Self::Why { json, parseable, .. } => *json || *parseable, + Self::Info { json, .. } => *json, + Self::Pm(sub) => sub.is_quiet_or_machine_readable(), + _ => false, + } + } + + /// Whether this invocation hits the vite-plus-managed-global flow on the + /// global CLI. The local CLI binding refuses these cases (it has no + /// managed package store of its own); pass-through `-g` cases like + /// `outdated -g`, `why -g`, and `pm config get -g` return `false` and + /// keep working on both CLIs. + pub fn is_managed_global(&self) -> bool { + match self { + Self::Install { global, .. } + | Self::Add { global, .. } + | Self::Remove { global, .. } + | Self::Update { global, .. } => *global, + Self::Pm(PmCommands::List { global, .. }) => *global, + _ => false, + } + } + + /// Determine the save dependency type from CLI flags shared by `Install` and `Add`. + pub fn determine_save_dependency_type( + save_dev: bool, + save_peer: bool, + save_optional: bool, + save_prod: bool, + ) -> Option { + if save_dev { + Some(SaveDependencyType::Dev) + } else if save_peer { + Some(SaveDependencyType::Peer) + } else if save_optional { + Some(SaveDependencyType::Optional) + } else if save_prod { + Some(SaveDependencyType::Production) + } else { + None + } + } +} + +/// Package manager subcommands (`vp pm `). +#[derive(Subcommand, Debug, Clone)] +pub enum PmCommands { + /// Remove unnecessary packages + Prune { + /// Remove devDependencies + #[arg(long)] + prod: bool, + + /// Remove optional dependencies + #[arg(long)] + no_optional: bool, + + /// Additional arguments + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// Create a tarball of the package + Pack { + /// Pack all workspace packages + #[arg(short = 'r', long)] + recursive: bool, + + /// Filter packages to pack + #[arg(long, value_name = "PATTERN")] + filter: Option>, + + /// Output path for the tarball + #[arg(long)] + out: Option, + + /// Directory where the tarball will be saved + #[arg(long)] + pack_destination: Option, + + /// Gzip compression level (0-9) + #[arg(long)] + pack_gzip_level: Option, + + /// Output in JSON format + #[arg(long)] + json: bool, + + /// Additional arguments + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// List installed packages + #[command(visible_alias = "ls")] + List { + /// Package pattern to filter + pattern: Option, + + /// Maximum depth of dependency tree + #[arg(long)] + depth: Option, + + /// Output in JSON format + #[arg(long)] + json: bool, + + /// Show extended information + #[arg(long)] + long: bool, + + /// Parseable output format + #[arg(long)] + parseable: bool, + + /// Only production dependencies + #[arg(short = 'P', long)] + prod: bool, + + /// Only dev dependencies + #[arg(short = 'D', long)] + dev: bool, + + /// Exclude optional dependencies + #[arg(long)] + no_optional: bool, + + /// Exclude peer dependencies + #[arg(long)] + exclude_peers: bool, + + /// Show only project packages + #[arg(long)] + only_projects: bool, + + /// Use a finder function + #[arg(long, value_name = "FINDER_NAME")] + find_by: Option, + + /// List across all workspaces + #[arg(short = 'r', long)] + recursive: bool, + + /// Filter packages in monorepo + #[arg(long, value_name = "PATTERN")] + filter: Vec, + + /// List global packages + #[arg(short = 'g', long)] + global: bool, + + /// Additional arguments + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// View package information from the registry + #[command(visible_alias = "info", visible_alias = "show")] + View { + /// Package name with optional version + #[arg(required = true)] + package: String, + + /// Specific field to view + field: Option, + + /// Output in JSON format + #[arg(long)] + json: bool, + + /// Additional arguments + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// Publish package to registry + Publish { + /// Tarball or folder to publish + #[arg(value_name = "TARBALL|FOLDER")] + target: Option, + + /// Preview without publishing + #[arg(long)] + dry_run: bool, + + /// Publish tag + #[arg(long)] + tag: Option, + + /// Access level (public/restricted) + #[arg(long)] + access: Option, + + /// One-time password for authentication + #[arg(long, value_name = "OTP")] + otp: Option, + + /// Skip git checks + #[arg(long)] + no_git_checks: bool, + + /// Set the branch name to publish from + #[arg(long, value_name = "BRANCH")] + publish_branch: Option, + + /// Save publish summary + #[arg(long)] + report_summary: bool, + + /// Force publish + #[arg(long)] + force: bool, + + /// Output in JSON format + #[arg(long)] + json: bool, + + /// Publish all workspace packages + #[arg(short = 'r', long)] + recursive: bool, + + /// Filter packages in monorepo + #[arg(long, value_name = "PATTERN")] + filter: Option>, + + /// Additional arguments + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// Manage package owners + #[command(subcommand, visible_alias = "author")] + Owner(OwnerCommands), + + /// Manage package cache + Cache { + /// Subcommand: dir, path, clean + #[arg(required = true)] + subcommand: String, + + /// Additional arguments + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// Manage package manager configuration + #[command(subcommand, visible_alias = "c")] + Config(ConfigCommands), + + /// Log in to a registry + #[command(visible_alias = "adduser")] + Login { + /// Registry URL + #[arg(long, value_name = "URL")] + registry: Option, + + /// Scope for the login + #[arg(long, value_name = "SCOPE")] + scope: Option, + + /// Additional arguments + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// Log out from a registry + Logout { + /// Registry URL + #[arg(long, value_name = "URL")] + registry: Option, + + /// Scope for the logout + #[arg(long, value_name = "SCOPE")] + scope: Option, + + /// Additional arguments + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// Show the current logged-in user + Whoami { + /// Registry URL + #[arg(long, value_name = "URL")] + registry: Option, + + /// Additional arguments + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// Manage authentication tokens + #[command(subcommand)] + Token(TokenCommands), + + /// Run a security audit + Audit { + /// Automatically fix vulnerabilities + #[arg(long)] + fix: bool, + + /// Output in JSON format + #[arg(long)] + json: bool, + + /// Minimum vulnerability level to report + #[arg(long, value_name = "LEVEL")] + level: Option, + + /// Only audit production dependencies + #[arg(long)] + production: bool, + + /// Additional arguments + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// Manage distribution tags + #[command(name = "dist-tag", subcommand)] + DistTag(DistTagCommands), + + /// Deprecate a package version + Deprecate { + /// Package name with version (e.g., "my-pkg@1.0.0") + package: String, + + /// Deprecation message + message: String, + + /// One-time password for authentication + #[arg(long, value_name = "OTP")] + otp: Option, + + /// Registry URL + #[arg(long, value_name = "URL")] + registry: Option, + + /// Additional arguments + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// Search for packages in the registry + Search { + /// Search terms + #[arg(required = true, num_args = 1..)] + terms: Vec, + + /// Output in JSON format + #[arg(long)] + json: bool, + + /// Show extended information + #[arg(long)] + long: bool, + + /// Registry URL + #[arg(long, value_name = "URL")] + registry: Option, + + /// Additional arguments + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// Rebuild native modules + #[command(visible_alias = "rb")] + Rebuild { + /// Additional arguments + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// Show funding information for installed packages + Fund { + /// Output in JSON format + #[arg(long)] + json: bool, + + /// Additional arguments + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// Ping the registry + Ping { + /// Registry URL + #[arg(long, value_name = "URL")] + registry: Option, + + /// Additional arguments + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, +} + +impl PmCommands { + pub fn is_quiet_or_machine_readable(&self) -> bool { + match self { + Self::List { json, parseable, .. } => *json || *parseable, + Self::Pack { json, .. } + | Self::View { json, .. } + | Self::Publish { json, .. } + | Self::Audit { json, .. } + | Self::Search { json, .. } + | Self::Fund { json, .. } => *json, + Self::Config(sub) => sub.is_quiet_or_machine_readable(), + Self::Token(sub) => sub.is_quiet_or_machine_readable(), + _ => false, + } + } +} + +/// Configuration subcommands. +#[derive(Subcommand, Debug, Clone)] +pub enum ConfigCommands { + /// List all configuration + List { + /// Output in JSON format + #[arg(long)] + json: bool, + + /// Use global config + #[arg(short = 'g', long)] + global: bool, + + /// Config location: project (default) or global + #[arg(long, value_name = "LOCATION")] + location: Option, + }, + + /// Get configuration value + Get { + /// Config key + key: String, + + /// Output in JSON format + #[arg(long)] + json: bool, + + /// Use global config + #[arg(short = 'g', long)] + global: bool, + + /// Config location + #[arg(long, value_name = "LOCATION")] + location: Option, + }, + + /// Set configuration value + Set { + /// Config key + key: String, + + /// Config value + value: String, + + /// Output in JSON format + #[arg(long)] + json: bool, + + /// Use global config + #[arg(short = 'g', long)] + global: bool, + + /// Config location + #[arg(long, value_name = "LOCATION")] + location: Option, + }, + + /// Delete configuration key + Delete { + /// Config key + key: String, + + /// Use global config + #[arg(short = 'g', long)] + global: bool, + + /// Config location + #[arg(long, value_name = "LOCATION")] + location: Option, + }, +} + +impl ConfigCommands { + pub fn is_quiet_or_machine_readable(&self) -> bool { + match self { + Self::List { json, .. } | Self::Get { json, .. } | Self::Set { json, .. } => *json, + _ => false, + } + } +} + +/// Owner subcommands. +#[derive(Subcommand, Debug, Clone)] +pub enum OwnerCommands { + /// List package owners + #[command(visible_alias = "ls")] + List { + /// Package name + package: String, + + /// One-time password for authentication + #[arg(long, value_name = "OTP")] + otp: Option, + }, + + /// Add package owner + Add { + /// Username + user: String, + /// Package name + package: String, + + /// One-time password for authentication + #[arg(long, value_name = "OTP")] + otp: Option, + }, + + /// Remove package owner + Rm { + /// Username + user: String, + /// Package name + package: String, + + /// One-time password for authentication + #[arg(long, value_name = "OTP")] + otp: Option, + }, +} + +/// Token subcommands. +#[derive(Subcommand, Debug, Clone)] +pub enum TokenCommands { + /// List all known tokens + #[command(visible_alias = "ls")] + List { + /// Output in JSON format + #[arg(long)] + json: bool, + + /// Registry URL + #[arg(long, value_name = "URL")] + registry: Option, + + /// Additional arguments + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// Create a new authentication token + Create { + /// Output in JSON format + #[arg(long)] + json: bool, + + /// Registry URL + #[arg(long, value_name = "URL")] + registry: Option, + + /// CIDR ranges to restrict the token to + #[arg(long, value_name = "CIDR")] + cidr: Option>, + + /// Create a read-only token + #[arg(long)] + readonly: bool, + + /// Additional arguments + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// Revoke an authentication token + Revoke { + /// Token or token ID to revoke + token: String, + + /// Registry URL + #[arg(long, value_name = "URL")] + registry: Option, + + /// Additional arguments + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, +} + +impl TokenCommands { + pub fn is_quiet_or_machine_readable(&self) -> bool { + match self { + Self::List { json, .. } | Self::Create { json, .. } => *json, + _ => false, + } + } +} + +/// Distribution tag subcommands. +#[derive(Subcommand, Debug, Clone)] +pub enum DistTagCommands { + /// List distribution tags for a package + #[command(visible_alias = "ls")] + List { + /// Package name + package: Option, + }, + + /// Add a distribution tag + Add { + /// Package name with version (e.g., "my-pkg@1.0.0") + package_at_version: String, + + /// Tag name + tag: String, + }, + + /// Remove a distribution tag + Rm { + /// Package name + package: String, + + /// Tag name + tag: String, + }, +} diff --git a/crates/vite_pm_cli/src/dispatch.rs b/crates/vite_pm_cli/src/dispatch.rs new file mode 100644 index 0000000000..a16851e3d1 --- /dev/null +++ b/crates/vite_pm_cli/src/dispatch.rs @@ -0,0 +1,315 @@ +//! Maps a parsed [`PackageManagerCommand`] to the appropriate handler. +//! +//! Callers must perform any environment setup (PATH adjustments, runtime +//! download) before invoking [`dispatch`]. + +use std::process::ExitStatus; + +use vite_install::commands::{ + add::AddCommandOptions, dedupe::DedupeCommandOptions, install::InstallCommandOptions, + link::LinkCommandOptions, outdated::OutdatedCommandOptions, remove::RemoveCommandOptions, + unlink::UnlinkCommandOptions, update::UpdateCommandOptions, view::ViewCommandOptions, + why::WhyCommandOptions, +}; +use vite_path::AbsolutePath; + +use crate::{cli::PackageManagerCommand, error::Error, handlers}; + +pub async fn dispatch( + cwd: &AbsolutePath, + command: PackageManagerCommand, +) -> Result { + match command { + PackageManagerCommand::Install { + prod, + dev, + no_optional, + frozen_lockfile, + no_frozen_lockfile, + lockfile_only, + prefer_offline, + offline, + force, + ignore_scripts, + no_lockfile, + fix_lockfile, + shamefully_hoist, + resolution_only, + silent, + filter, + workspace_root, + save_exact, + save_peer, + save_optional, + save_catalog, + global, + node: _, + packages, + pass_through_args, + } => { + // `vp install ` is an alias for `vp add `. + if let Some(pkgs) = packages + && !pkgs.is_empty() + { + let save_dependency_type = PackageManagerCommand::determine_save_dependency_type( + dev, + save_peer, + save_optional, + prod, + ); + let options = AddCommandOptions { + packages: &pkgs, + save_dependency_type, + save_exact, + save_catalog_name: catalog_name(save_catalog, None), + filters: filter.as_deref(), + workspace_root, + workspace_only: false, + global, + allow_build: None, + pass_through_args: pass_through_args.as_deref(), + }; + return handlers::run_add(cwd, &options).await; + } + + let options = InstallCommandOptions { + prod, + dev, + no_optional, + frozen_lockfile, + no_frozen_lockfile, + lockfile_only, + prefer_offline, + offline, + force, + ignore_scripts, + no_lockfile, + fix_lockfile, + shamefully_hoist, + resolution_only, + silent, + filters: filter.as_deref(), + workspace_root, + pass_through_args: pass_through_args.as_deref(), + }; + handlers::run_install(cwd, &options).await + } + + PackageManagerCommand::Add { + save_prod, + save_dev, + save_peer, + save_optional, + save_exact, + save_catalog_name, + save_catalog, + allow_build, + filter, + workspace_root, + workspace, + global, + node: _, + packages, + pass_through_args, + } => { + let save_dependency_type = PackageManagerCommand::determine_save_dependency_type( + save_dev, + save_peer, + save_optional, + save_prod, + ); + let options = AddCommandOptions { + packages: &packages, + save_dependency_type, + save_exact, + save_catalog_name: catalog_name(save_catalog, save_catalog_name.as_deref()), + filters: filter.as_deref(), + workspace_root, + workspace_only: workspace, + global, + allow_build: allow_build.as_deref(), + pass_through_args: pass_through_args.as_deref(), + }; + handlers::run_add(cwd, &options).await + } + + PackageManagerCommand::Remove { + save_dev, + save_optional, + save_prod, + filter, + workspace_root, + recursive, + global, + // `--dry-run` is clap-required to coexist with `-g`, and `-g` is + // either intercepted by the global CLI's `run_package_manager_command` + // (managed flow) or rejected by the local CLI binding's + // `execute_pm_command`. Either way, this arm only sees `dry_run: false`. + dry_run: _, + packages, + pass_through_args, + } => { + let options = RemoveCommandOptions { + packages: &packages, + filters: filter.as_deref(), + workspace_root, + recursive, + global, + save_dev, + save_optional, + save_prod, + pass_through_args: pass_through_args.as_deref(), + }; + handlers::run_remove(cwd, &options).await + } + + PackageManagerCommand::Update { + latest, + global: _, + recursive, + filter, + workspace_root, + dev, + prod, + interactive, + no_optional, + no_save, + workspace, + packages, + pass_through_args, + } => { + let options = UpdateCommandOptions { + packages: &packages, + latest, + recursive, + filters: filter.as_deref(), + workspace_root, + dev, + prod, + interactive, + no_optional, + no_save, + workspace_only: workspace, + pass_through_args: pass_through_args.as_deref(), + }; + handlers::run_update(cwd, &options).await + } + + PackageManagerCommand::Dedupe { check, pass_through_args } => { + let options = + DedupeCommandOptions { check, pass_through_args: pass_through_args.as_deref() }; + handlers::run_dedupe(cwd, &options).await + } + + PackageManagerCommand::Outdated { + packages, + long, + format, + recursive, + filter, + workspace_root, + prod, + dev, + no_optional, + compatible, + sort_by, + global, + pass_through_args, + } => { + let options = OutdatedCommandOptions { + packages: &packages, + long, + format, + recursive, + filters: filter.as_deref(), + workspace_root, + prod, + dev, + no_optional, + compatible, + sort_by: sort_by.as_deref(), + global, + pass_through_args: pass_through_args.as_deref(), + }; + handlers::run_outdated(cwd, &options).await + } + + PackageManagerCommand::Why { + packages, + json, + long, + parseable, + recursive, + filter, + workspace_root, + prod, + dev, + depth, + no_optional, + global, + exclude_peers, + find_by, + pass_through_args, + } => { + let options = WhyCommandOptions { + packages: &packages, + json, + long, + parseable, + recursive, + filters: filter.as_deref(), + workspace_root, + prod, + dev, + depth, + no_optional, + global, + exclude_peers, + find_by: find_by.as_deref(), + pass_through_args: pass_through_args.as_deref(), + }; + handlers::run_why(cwd, &options).await + } + + PackageManagerCommand::Info { package, field, json, pass_through_args } => { + let options = ViewCommandOptions { + package: &package, + field: field.as_deref(), + json, + pass_through_args: pass_through_args.as_deref(), + }; + handlers::run_info(cwd, &options).await + } + + PackageManagerCommand::Link { package, args } => { + let options = LinkCommandOptions { + package: package.as_deref(), + pass_through_args: pass_through_slice(&args), + }; + handlers::run_link(cwd, &options).await + } + + PackageManagerCommand::Unlink { package, recursive, args } => { + let options = UnlinkCommandOptions { + package: package.as_deref(), + recursive, + pass_through_args: pass_through_slice(&args), + }; + handlers::run_unlink(cwd, &options).await + } + + PackageManagerCommand::Dlx { package, shell_mode, silent, args } => { + handlers::run_dlx(cwd, package, shell_mode, silent, args).await + } + + PackageManagerCommand::Pm(pm_command) => handlers::run_pm_subcommand(cwd, pm_command).await, + } +} + +fn catalog_name<'a>(save_catalog: bool, save_catalog_name: Option<&'a str>) -> Option<&'a str> { + if save_catalog { Some("default") } else { save_catalog_name } +} + +fn pass_through_slice(args: &[String]) -> Option<&[String]> { + if args.is_empty() { None } else { Some(args) } +} diff --git a/crates/vite_pm_cli/src/error.rs b/crates/vite_pm_cli/src/error.rs new file mode 100644 index 0000000000..108116c860 --- /dev/null +++ b/crates/vite_pm_cli/src/error.rs @@ -0,0 +1,29 @@ +use std::io; + +use vite_str::Str; + +/// Error type returned by the PM dispatcher. +/// +/// Both the global CLI and the local CLI binding wrap this in their own +/// error enums via `#[from]`. +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("{0}")] + Install(#[from] vite_error::Error), + + #[error("Workspace error: {0}")] + Workspace(#[from] vite_workspace::Error), + + #[error("Command execution failed: {0}")] + CommandExecution(#[from] io::Error), + + #[error("JSON error: {0}")] + Json(#[from] serde_json::Error), + + /// User-facing message printed without the "Error: " prefix. + #[error("{0}")] + UserMessage(Str), + + #[error("{0}")] + Other(Str), +} diff --git a/crates/vite_global_cli/src/commands/pm.rs b/crates/vite_pm_cli/src/handlers.rs similarity index 58% rename from crates/vite_global_cli/src/commands/pm.rs rename to crates/vite_pm_cli/src/handlers.rs index 85b8320594..585a056385 100644 --- a/crates/vite_global_cli/src/commands/pm.rs +++ b/crates/vite_pm_cli/src/handlers.rs @@ -1,72 +1,170 @@ -//! Package manager commands (Category A). -//! -//! This module handles the `pm` subcommand and the `info` command which are -//! routed through helper functions. Other PM commands (add, install, remove, etc.) -//! are implemented as separate command modules with struct-based patterns. - -use std::process::ExitStatus; - -use vite_install::commands::{ - audit::AuditCommandOptions, - cache::CacheCommandOptions, - config::ConfigCommandOptions, - deprecate::DeprecateCommandOptions, - dist_tag::{DistTagCommandOptions, DistTagSubcommand}, - fund::FundCommandOptions, - list::ListCommandOptions, - login::LoginCommandOptions, - logout::LogoutCommandOptions, - owner::OwnerSubcommand, - pack::PackCommandOptions, - ping::PingCommandOptions, - prune::PruneCommandOptions, - publish::PublishCommandOptions, - rebuild::RebuildCommandOptions, - search::SearchCommandOptions, - token::TokenSubcommand, - view::ViewCommandOptions, - whoami::WhoamiCommandOptions, +//! Handlers that wrap `vite_install`'s `PackageManager::run_*_command` +//! family, returning the underlying process exit status. + +use std::{collections::HashMap, process::ExitStatus}; + +use vite_command::run_command; +use vite_install::{ + PackageManager, + commands::{ + add::AddCommandOptions, + audit::AuditCommandOptions, + cache::CacheCommandOptions, + config::ConfigCommandOptions, + dedupe::DedupeCommandOptions, + deprecate::DeprecateCommandOptions, + dist_tag::{DistTagCommandOptions, DistTagSubcommand}, + dlx::{DlxCommandOptions, build_npx_args}, + fund::FundCommandOptions, + install::InstallCommandOptions, + link::LinkCommandOptions, + list::ListCommandOptions, + login::LoginCommandOptions, + logout::LogoutCommandOptions, + outdated::OutdatedCommandOptions, + owner::OwnerSubcommand, + pack::PackCommandOptions, + ping::PingCommandOptions, + prune::PruneCommandOptions, + publish::PublishCommandOptions, + rebuild::RebuildCommandOptions, + remove::RemoveCommandOptions, + search::SearchCommandOptions, + token::TokenSubcommand, + unlink::UnlinkCommandOptions, + update::UpdateCommandOptions, + view::ViewCommandOptions, + whoami::WhoamiCommandOptions, + why::WhyCommandOptions, + }, }; -use vite_path::AbsolutePathBuf; +use vite_path::AbsolutePath; -use super::{ - build_package_manager, build_package_manager_or_npm_default, prepend_js_runtime_to_path_env, -}; use crate::{ cli::{ConfigCommands, DistTagCommands, OwnerCommands, PmCommands, TokenCommands}, error::Error, + helpers::{build_package_manager, build_package_manager_or_npm_default, ensure_package_json}, }; -/// Execute the info command. -pub async fn execute_info( - cwd: AbsolutePathBuf, - package: &str, - field: Option<&str>, - json: bool, - pass_through_args: Option<&[String]>, +pub async fn run_add( + cwd: &AbsolutePath, + options: &AddCommandOptions<'_>, ) -> Result { - prepend_js_runtime_to_path_env(&cwd).await?; + ensure_package_json(cwd).await?; + let pm = PackageManager::builder(cwd).build_with_default().await?; + Ok(pm.run_add_command(options, cwd).await?) +} - let package_manager = build_package_manager_or_npm_default(&cwd).await?; +pub async fn run_install( + cwd: &AbsolutePath, + options: &InstallCommandOptions<'_>, +) -> Result { + ensure_package_json(cwd).await?; + let pm = PackageManager::builder(cwd).build_with_default().await?; + Ok(pm.run_install_command(options, cwd).await?) +} - let options = ViewCommandOptions { package, field, json, pass_through_args }; +pub async fn run_remove( + cwd: &AbsolutePath, + options: &RemoveCommandOptions<'_>, +) -> Result { + let pm = build_package_manager(cwd).await?; + Ok(pm.run_remove_command(options, cwd).await?) +} - Ok(package_manager.run_view_command(&options, &cwd).await?) +pub async fn run_update( + cwd: &AbsolutePath, + options: &UpdateCommandOptions<'_>, +) -> Result { + let pm = build_package_manager(cwd).await?; + Ok(pm.run_update_command(options, cwd).await?) } -/// Execute a pm subcommand. -pub async fn execute_pm_subcommand( - cwd: AbsolutePathBuf, - command: PmCommands, +pub async fn run_dedupe( + cwd: &AbsolutePath, + options: &DedupeCommandOptions<'_>, +) -> Result { + let pm = build_package_manager(cwd).await?; + Ok(pm.run_dedupe_command(options, cwd).await?) +} + +pub async fn run_outdated( + cwd: &AbsolutePath, + options: &OutdatedCommandOptions<'_>, +) -> Result { + let pm = build_package_manager(cwd).await?; + Ok(pm.run_outdated_command(options, cwd).await?) +} + +pub async fn run_why( + cwd: &AbsolutePath, + options: &WhyCommandOptions<'_>, +) -> Result { + let pm = build_package_manager(cwd).await?; + Ok(pm.run_why_command(options, cwd).await?) +} + +pub async fn run_info( + cwd: &AbsolutePath, + options: &ViewCommandOptions<'_>, +) -> Result { + let pm = build_package_manager_or_npm_default(cwd).await?; + Ok(pm.run_view_command(options, cwd).await?) +} + +pub async fn run_link( + cwd: &AbsolutePath, + options: &LinkCommandOptions<'_>, +) -> Result { + let pm = build_package_manager(cwd).await?; + Ok(pm.run_link_command(options, cwd).await?) +} + +pub async fn run_unlink( + cwd: &AbsolutePath, + options: &UnlinkCommandOptions<'_>, ) -> Result { - // Intercept `pm list -g` to use vite-plus managed global packages listing - if let PmCommands::List { global: true, json, ref pattern, .. } = command { - return crate::commands::env::packages::execute(json, pattern.as_deref()).await; + let pm = build_package_manager(cwd).await?; + Ok(pm.run_unlink_command(options, cwd).await?) +} + +pub async fn run_dlx( + cwd: &AbsolutePath, + packages: Vec, + shell_mode: bool, + silent: bool, + args: Vec, +) -> Result { + if args.is_empty() { + return Err(Error::Other("dlx requires a package name".into())); } - prepend_js_runtime_to_path_env(&cwd).await?; + let package_spec = &args[0]; + let command_args: Vec = args[1..].to_vec(); + + let options = DlxCommandOptions { + packages: &packages, + package_spec, + args: &command_args, + shell_mode, + silent, + }; + + match PackageManager::builder(cwd).build_with_default().await { + Ok(pm) => Ok(pm.run_dlx_command(&options, cwd).await?), + Err(vite_error::Error::WorkspaceError(vite_workspace::Error::PackageJsonNotFound(_))) => { + let npx_args = build_npx_args(&options); + let envs = HashMap::new(); + Ok(run_command("npx", &npx_args, &envs, cwd).await?) + } + Err(e) => Err(Error::Install(e)), + } +} - // Project-dependent commands require package.json; standalone ones fall back to npm. +pub async fn run_pm_subcommand( + cwd: &AbsolutePath, + command: PmCommands, +) -> Result { let needs_project = matches!( command, PmCommands::Prune { .. } @@ -78,10 +176,10 @@ pub async fn execute_pm_subcommand( | PmCommands::Audit { .. } ); - let package_manager = if needs_project { - build_package_manager(&cwd).await? + let pm = if needs_project { + build_package_manager(cwd).await? } else { - build_package_manager_or_npm_default(&cwd).await? + build_package_manager_or_npm_default(cwd).await? }; match command { @@ -91,7 +189,7 @@ pub async fn execute_pm_subcommand( no_optional, pass_through_args: pass_through_args.as_deref(), }; - Ok(package_manager.run_prune_command(&options, &cwd).await?) + Ok(pm.run_prune_command(&options, cwd).await?) } PmCommands::Pack { @@ -112,7 +210,7 @@ pub async fn execute_pm_subcommand( json, pass_through_args: pass_through_args.as_deref(), }; - Ok(package_manager.run_pack_command(&options, &cwd).await?) + Ok(pm.run_pack_command(&options, cwd).await?) } PmCommands::List { @@ -149,7 +247,7 @@ pub async fn execute_pm_subcommand( global, pass_through_args: pass_through_args.as_deref(), }; - Ok(package_manager.run_list_command(&options, &cwd).await?) + Ok(pm.run_list_command(&options, cwd).await?) } PmCommands::View { package, field, json, pass_through_args } => { @@ -159,7 +257,7 @@ pub async fn execute_pm_subcommand( json, pass_through_args: pass_through_args.as_deref(), }; - Ok(package_manager.run_view_command(&options, &cwd).await?) + Ok(pm.run_view_command(&options, cwd).await?) } PmCommands::Publish { @@ -192,7 +290,7 @@ pub async fn execute_pm_subcommand( filters: filter.as_deref(), pass_through_args: pass_through_args.as_deref(), }; - Ok(package_manager.run_publish_command(&options, &cwd).await?) + Ok(pm.run_publish_command(&options, cwd).await?) } PmCommands::Owner(owner_command) => { @@ -205,7 +303,7 @@ pub async fn execute_pm_subcommand( OwnerSubcommand::Rm { user, package, otp } } }; - Ok(package_manager.run_owner_command(&subcommand, &cwd).await?) + Ok(pm.run_owner_command(&subcommand, cwd).await?) } PmCommands::Cache { subcommand, pass_through_args } => { @@ -213,7 +311,7 @@ pub async fn execute_pm_subcommand( subcommand: &subcommand, pass_through_args: pass_through_args.as_deref(), }; - Ok(package_manager.run_cache_command(&options, &cwd).await?) + Ok(pm.run_cache_command(&options, cwd).await?) } PmCommands::Config(config_command) => match config_command { @@ -223,10 +321,10 @@ pub async fn execute_pm_subcommand( key: None, value: None, json, - location: if global { Some("global") } else { location.as_deref() }, + location: config_location(global, location.as_deref()), pass_through_args: None, }; - Ok(package_manager.run_config_command(&options, &cwd).await?) + Ok(pm.run_config_command(&options, cwd).await?) } ConfigCommands::Get { key, json, global, location } => { let options = ConfigCommandOptions { @@ -234,10 +332,10 @@ pub async fn execute_pm_subcommand( key: Some(key.as_str()), value: None, json, - location: if global { Some("global") } else { location.as_deref() }, + location: config_location(global, location.as_deref()), pass_through_args: None, }; - Ok(package_manager.run_config_command(&options, &cwd).await?) + Ok(pm.run_config_command(&options, cwd).await?) } ConfigCommands::Set { key, value, json, global, location } => { let options = ConfigCommandOptions { @@ -245,10 +343,10 @@ pub async fn execute_pm_subcommand( key: Some(key.as_str()), value: Some(value.as_str()), json, - location: if global { Some("global") } else { location.as_deref() }, + location: config_location(global, location.as_deref()), pass_through_args: None, }; - Ok(package_manager.run_config_command(&options, &cwd).await?) + Ok(pm.run_config_command(&options, cwd).await?) } ConfigCommands::Delete { key, global, location } => { let options = ConfigCommandOptions { @@ -256,10 +354,10 @@ pub async fn execute_pm_subcommand( key: Some(key.as_str()), value: None, json: false, - location: if global { Some("global") } else { location.as_deref() }, + location: config_location(global, location.as_deref()), pass_through_args: None, }; - Ok(package_manager.run_config_command(&options, &cwd).await?) + Ok(pm.run_config_command(&options, cwd).await?) } }, @@ -269,7 +367,7 @@ pub async fn execute_pm_subcommand( scope: scope.as_deref(), pass_through_args: pass_through_args.as_deref(), }; - Ok(package_manager.run_login_command(&options, &cwd).await?) + Ok(pm.run_login_command(&options, cwd).await?) } PmCommands::Logout { registry, scope, pass_through_args } => { @@ -278,7 +376,7 @@ pub async fn execute_pm_subcommand( scope: scope.as_deref(), pass_through_args: pass_through_args.as_deref(), }; - Ok(package_manager.run_logout_command(&options, &cwd).await?) + Ok(pm.run_logout_command(&options, cwd).await?) } PmCommands::Whoami { registry, pass_through_args } => { @@ -286,7 +384,7 @@ pub async fn execute_pm_subcommand( registry: registry.as_deref(), pass_through_args: pass_through_args.as_deref(), }; - Ok(package_manager.run_whoami_command(&options, &cwd).await?) + Ok(pm.run_whoami_command(&options, cwd).await?) } PmCommands::Token(token_command) => { @@ -301,7 +399,7 @@ pub async fn execute_pm_subcommand( TokenSubcommand::Revoke { token, registry, pass_through_args } } }; - Ok(package_manager.run_token_command(&subcommand, &cwd).await?) + Ok(pm.run_token_command(&subcommand, cwd).await?) } PmCommands::Audit { fix, json, level, production, pass_through_args } => { @@ -312,7 +410,7 @@ pub async fn execute_pm_subcommand( production, pass_through_args: pass_through_args.as_deref(), }; - Ok(package_manager.run_audit_command(&options, &cwd).await?) + Ok(pm.run_audit_command(&options, cwd).await?) } PmCommands::DistTag(dist_tag_command) => { @@ -324,7 +422,7 @@ pub async fn execute_pm_subcommand( DistTagCommands::Rm { package, tag } => DistTagSubcommand::Rm { package, tag }, }; let options = DistTagCommandOptions { subcommand, pass_through_args: None }; - Ok(package_manager.run_dist_tag_command(&options, &cwd).await?) + Ok(pm.run_dist_tag_command(&options, cwd).await?) } PmCommands::Deprecate { package, message, otp, registry, pass_through_args } => { @@ -335,7 +433,7 @@ pub async fn execute_pm_subcommand( registry: registry.as_deref(), pass_through_args: pass_through_args.as_deref(), }; - Ok(package_manager.run_deprecate_command(&options, &cwd).await?) + Ok(pm.run_deprecate_command(&options, cwd).await?) } PmCommands::Search { terms, json, long, registry, pass_through_args } => { @@ -346,18 +444,18 @@ pub async fn execute_pm_subcommand( registry: registry.as_deref(), pass_through_args: pass_through_args.as_deref(), }; - Ok(package_manager.run_search_command(&options, &cwd).await?) + Ok(pm.run_search_command(&options, cwd).await?) } PmCommands::Rebuild { pass_through_args } => { let options = RebuildCommandOptions { pass_through_args: pass_through_args.as_deref() }; - Ok(package_manager.run_rebuild_command(&options, &cwd).await?) + Ok(pm.run_rebuild_command(&options, cwd).await?) } PmCommands::Fund { json, pass_through_args } => { let options = FundCommandOptions { json, pass_through_args: pass_through_args.as_deref() }; - Ok(package_manager.run_fund_command(&options, &cwd).await?) + Ok(pm.run_fund_command(&options, cwd).await?) } PmCommands::Ping { registry, pass_through_args } => { @@ -365,18 +463,11 @@ pub async fn execute_pm_subcommand( registry: registry.as_deref(), pass_through_args: pass_through_args.as_deref(), }; - Ok(package_manager.run_ping_command(&options, &cwd).await?) + Ok(pm.run_ping_command(&options, cwd).await?) } } } -#[cfg(test)] -mod tests { - use vite_install::commands::add::SaveDependencyType; - - #[test] - fn test_save_dependency_type() { - assert!(matches!(SaveDependencyType::Dev, SaveDependencyType::Dev)); - assert!(matches!(SaveDependencyType::Production, SaveDependencyType::Production)); - } +fn config_location(global: bool, location: Option<&str>) -> Option<&str> { + if global { Some("global") } else { location } } diff --git a/crates/vite_pm_cli/src/helpers.rs b/crates/vite_pm_cli/src/helpers.rs new file mode 100644 index 0000000000..fdf35f39a9 --- /dev/null +++ b/crates/vite_pm_cli/src/helpers.rs @@ -0,0 +1,71 @@ +//! Shared helpers used by every PM handler. + +use vite_install::package_manager::{PackageManager, PackageManagerType}; +use vite_path::AbsolutePath; + +use crate::error::Error; + +/// Build a `PackageManager`, converting `PackageJsonNotFound` into a +/// friendly error message. +pub async fn build_package_manager(cwd: &AbsolutePath) -> Result { + match PackageManager::builder(cwd).build_with_default().await { + Ok(pm) => Ok(pm), + Err(vite_error::Error::WorkspaceError(vite_workspace::Error::PackageJsonNotFound(_))) => { + Err(Error::UserMessage("No package.json found.".into())) + } + Err(e) => Err(Error::Install(e)), + } +} + +/// Build a `PackageManager`, falling back to a default npm instance when no +/// package.json is found. Uses `build()` instead of `build_with_default()` +/// to skip the interactive package manager selection prompt on the fallback path. +/// +/// Callers should ensure npm is on PATH before invoking commands that hit +/// this fallback (the global CLI does this via its managed Node runtime; +/// the local CLI relies on the system Node). +pub async fn build_package_manager_or_npm_default( + cwd: &AbsolutePath, +) -> Result { + match PackageManager::builder(cwd).build().await { + Ok(pm) => Ok(pm), + Err(vite_error::Error::WorkspaceError(vite_workspace::Error::PackageJsonNotFound(_))) + | Err(vite_error::Error::UnrecognizedPackageManager) => { + Ok(default_npm_package_manager(cwd)) + } + Err(e) => Err(Error::Install(e)), + } +} + +fn default_npm_package_manager(cwd: &AbsolutePath) -> PackageManager { + PackageManager { + client: PackageManagerType::Npm, + package_name: "npm".into(), + version: "latest".into(), + hash: None, + bin_name: "npm".into(), + workspace_root: cwd.to_absolute_path_buf(), + is_monorepo: false, + install_dir: cwd.to_absolute_path_buf(), + } +} + +/// Ensure a package.json exists in the given directory. +/// If it doesn't exist, create a minimal one with `{ "type": "module" }`. +pub async fn ensure_package_json(project_path: &AbsolutePath) -> Result<(), Error> { + use tokio::io::AsyncWriteExt; + + let package_json_path = project_path.join("package.json"); + let content = serde_json::to_string_pretty(&serde_json::json!({ "type": "module" }))?; + match tokio::fs::OpenOptions::new().write(true).create_new(true).open(&package_json_path).await + { + Ok(mut file) => { + file.write_all(content.as_bytes()).await?; + file.write_all(b"\n").await?; + tracing::info!("Created package.json in {:?}", project_path); + Ok(()) + } + Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => Ok(()), + Err(err) => Err(Error::CommandExecution(err)), + } +} diff --git a/crates/vite_pm_cli/src/lib.rs b/crates/vite_pm_cli/src/lib.rs new file mode 100644 index 0000000000..85c84353a0 --- /dev/null +++ b/crates/vite_pm_cli/src/lib.rs @@ -0,0 +1,19 @@ +//! Shared clap surface and dispatcher for `vp`'s package-manager +//! subcommands (`install`, `add`, `remove`, `update`, `dlx`, `pm …`, …). +//! +//! Both the global CLI and the local NAPI binding flatten +//! [`PackageManagerCommand`] into their top-level argument parser and call +//! [`dispatch`] to execute the parsed command. The crate does not do any +//! managed-Node-runtime or managed-global-install handling — those stay in +//! the global CLI; PM operations always go through whichever package +//! manager (pnpm/npm/yarn/bun) is detected for the project. + +pub mod cli; +pub mod dispatch; +pub mod error; +pub mod handlers; +pub mod helpers; + +pub use cli::PackageManagerCommand; +pub use dispatch::dispatch; +pub use error::Error; diff --git a/crates/vite_shared/src/output.rs b/crates/vite_shared/src/output.rs index b5615954e9..2ba6c2e2ed 100644 --- a/crates/vite_shared/src/output.rs +++ b/crates/vite_shared/src/output.rs @@ -62,3 +62,9 @@ pub fn raw(msg: &str) { pub fn raw_inline(msg: &str) { print!("{msg}"); } + +/// Print a raw message to stderr with no prefix or formatting. +#[allow(clippy::print_stderr, clippy::disallowed_macros)] +pub fn raw_stderr(msg: &str) { + eprintln!("{msg}"); +} diff --git a/packages/cli/binding/Cargo.toml b/packages/cli/binding/Cargo.toml index ef859dfd81..85a75f4595 100644 --- a/packages/cli/binding/Cargo.toml +++ b/packages/cli/binding/Cargo.toml @@ -29,6 +29,7 @@ vite_command = { workspace = true } vite_error = { workspace = true } vite_install = { workspace = true } vite_migration = { workspace = true } +vite_pm_cli = { workspace = true } vite_path = { workspace = true } vite_shared = { workspace = true } vite_static_config = { workspace = true } diff --git a/packages/cli/binding/src/check/mod.rs b/packages/cli/binding/src/check/mod.rs index 607600242e..f435bf81d9 100644 --- a/packages/cli/binding/src/check/mod.rs +++ b/packages/cli/binding/src/check/mod.rs @@ -4,7 +4,7 @@ use std::{ffi::OsStr, sync::Arc, time::Instant}; use rustc_hash::FxHashMap; use vite_error::Error; -use vite_path::{AbsolutePath, AbsolutePathBuf}; +use vite_path::AbsolutePathBuf; use vite_shared::output; use vite_task::ExitStatus; @@ -27,7 +27,6 @@ pub(crate) async fn execute_check( paths: Vec, envs: &Arc, Arc>>, cwd: &AbsolutePathBuf, - cwd_arc: &Arc, ) -> Result { let mut status = ExitStatus::SUCCESS; let has_paths = !paths.is_empty(); @@ -69,7 +68,6 @@ pub(crate) async fn execute_check( Some(&resolved_vite_config), envs, cwd, - cwd_arc, false, ) .await?; @@ -161,7 +159,6 @@ pub(crate) async fn execute_check( Some(&resolved_vite_config), envs, cwd, - cwd_arc, true, ) .await?; @@ -240,7 +237,6 @@ pub(crate) async fn execute_check( Some(&resolved_vite_config), envs, cwd, - cwd_arc, false, ) .await?; diff --git a/packages/cli/binding/src/cli/execution.rs b/packages/cli/binding/src/cli/execution.rs index e8840c2d23..5a5811b23d 100644 --- a/packages/cli/binding/src/cli/execution.rs +++ b/packages/cli/binding/src/cli/execution.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, ffi::OsStr, io::IsTerminal, process::Stdio, sync::Arc}; use rustc_hash::FxHashMap; use vite_error::Error; -use vite_path::{AbsolutePath, AbsolutePathBuf}; +use vite_path::AbsolutePathBuf; use vite_task::ExitStatus; use super::{ @@ -17,10 +17,9 @@ async fn resolve_and_build_command( resolved_vite_config: Option<&ResolvedUniversalViteConfig>, envs: &Arc, Arc>>, cwd: &AbsolutePathBuf, - cwd_arc: &Arc, ) -> Result { let resolved = resolver - .resolve(subcommand, resolved_vite_config, envs, cwd_arc) + .resolve(subcommand, resolved_vite_config, envs) .await .map_err(|e| Error::Anyhow(e))?; @@ -55,7 +54,6 @@ pub(super) async fn resolve_and_execute( resolved_vite_config: Option<&ResolvedUniversalViteConfig>, envs: &Arc, Arc>>, cwd: &AbsolutePathBuf, - cwd_arc: &Arc, ) -> Result { let is_interactive = matches!( subcommand, @@ -63,8 +61,7 @@ pub(super) async fn resolve_and_execute( ); let mut cmd = - resolve_and_build_command(resolver, subcommand, resolved_vite_config, envs, cwd, cwd_arc) - .await?; + resolve_and_build_command(resolver, subcommand, resolved_vite_config, envs, cwd).await?; // For interactive commands (dev, preview), use terminal guard to restore terminal state on exit if is_interactive { @@ -90,13 +87,11 @@ pub(super) async fn resolve_and_execute_with_filter( resolved_vite_config: Option<&ResolvedUniversalViteConfig>, envs: &Arc, Arc>>, cwd: &AbsolutePathBuf, - cwd_arc: &Arc, stream: FilterStream, filter: impl Fn(&str) -> Cow<'_, str>, ) -> Result { let mut cmd = - resolve_and_build_command(resolver, subcommand, resolved_vite_config, envs, cwd, cwd_arc) - .await?; + resolve_and_build_command(resolver, subcommand, resolved_vite_config, envs, cwd).await?; match stream { FilterStream::Stdout => cmd.stdout(Stdio::piped()), FilterStream::Stderr => cmd.stderr(Stdio::piped()), @@ -126,12 +121,10 @@ pub(crate) async fn resolve_and_capture_output( resolved_vite_config: Option<&ResolvedUniversalViteConfig>, envs: &Arc, Arc>>, cwd: &AbsolutePathBuf, - cwd_arc: &Arc, force_color_if_terminal: bool, ) -> Result { let mut cmd = - resolve_and_build_command(resolver, subcommand, resolved_vite_config, envs, cwd, cwd_arc) - .await?; + resolve_and_build_command(resolver, subcommand, resolved_vite_config, envs, cwd).await?; cmd.stdout(Stdio::piped()); cmd.stderr(Stdio::piped()); if force_color_if_terminal && std::io::stdout().is_terminal() { diff --git a/packages/cli/binding/src/cli/handler.rs b/packages/cli/binding/src/cli/handler.rs index 1b7f17ba80..77d9096ef4 100644 --- a/packages/cli/binding/src/cli/handler.rs +++ b/packages/cli/binding/src/cli/handler.rs @@ -71,13 +71,13 @@ impl CommandHandler for VitePlusCommandHandler { ))) } CLIArgs::Synthesizable(subcmd) => { - let resolved = - self.resolver.resolve(subcmd, None, &command.envs, &command.cwd).await?; + let resolved = self.resolver.resolve(subcmd, None, &command.envs).await?; Ok(HandledCommand::Synthesized(resolved.into_synthetic_plan_request())) } CLIArgs::ViteTask(cmd) => Ok(HandledCommand::ViteTaskCommand(cmd)), - CLIArgs::Exec(_) => { - // exec in task scripts should run as a subprocess + CLIArgs::PackageManager(_) | CLIArgs::Exec(_) => { + // PM commands and exec in task scripts run as subprocesses + // — no caching, no synthesis through the resolver. Ok(HandledCommand::Synthesized( command.to_synthetic_plan_request(UserCacheConfig::disabled()), )) diff --git a/packages/cli/binding/src/cli/mod.rs b/packages/cli/binding/src/cli/mod.rs index f94443964b..76dbc72dab 100644 --- a/packages/cli/binding/src/cli/mod.rs +++ b/packages/cli/binding/src/cli/mod.rs @@ -60,7 +60,6 @@ async fn execute_direct_subcommand( .map(|(k, v)| (Arc::from(k.as_os_str()), Arc::from(v.as_os_str()))) .collect(), ); - let cwd_arc: Arc = cwd.clone().into(); let status = match subcommand { SynthesizableSubcommand::Check { @@ -79,7 +78,6 @@ async fn execute_direct_subcommand( paths, &envs, cwd, - &cwd_arc, ) .await; } @@ -91,7 +89,6 @@ async fn execute_direct_subcommand( None, &envs, cwd, - &cwd_arc, FilterStream::Stdout, |_| Cow::Borrowed(""), ) @@ -103,13 +100,12 @@ async fn execute_direct_subcommand( None, &envs, cwd, - &cwd_arc, FilterStream::Stderr, |s| s.cow_replace("oxfmt --init", "vp fmt --init"), ) .await? } else { - resolve_and_execute(&resolver, other, None, &envs, cwd, &cwd_arc).await? + resolve_and_execute(&resolver, other, None, &envs, cwd).await? } } }; @@ -191,10 +187,32 @@ pub async fn main( match cli_args { CLIArgs::Synthesizable(subcmd) => execute_direct_subcommand(subcmd, &cwd, options).await, CLIArgs::ViteTask(command) => execute_vite_task_command(command, cwd, options).await, + CLIArgs::PackageManager(pm) => execute_pm_command(pm, &cwd).await, CLIArgs::Exec(exec_args) => crate::exec::execute(exec_args, &cwd).await, } } +/// Execute a package-manager command directly through `vite_pm_cli`, +/// bypassing the vite-task scheduler — PM operations don't need caching. +async fn execute_pm_command( + command: vite_pm_cli::PackageManagerCommand, + cwd: &AbsolutePath, +) -> Result { + // `-g`/`--global` operations on install/add/remove/update/`pm list` map to + // a vite-plus-managed package store on the global CLI; the local CLI has + // no such store, so refuse rather than silently doing the wrong thing + // (mutating the project, dropping `--node`, ignoring `--dry-run`, …). + if command.is_managed_global() { + return Err(Error::Anyhow(anyhow::anyhow!( + "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.", + ))); + } + let status = vite_pm_cli::dispatch(cwd, command) + .await + .map_err(|e| Error::Anyhow(anyhow::Error::new(e)))?; + Ok(ExitStatus(status.code().unwrap_or(1) as u8)) +} + #[cfg(test)] mod tests { use std::path::PathBuf; diff --git a/packages/cli/binding/src/cli/resolver.rs b/packages/cli/binding/src/cli/resolver.rs index db5dd3727b..5208fc7a42 100644 --- a/packages/cli/binding/src/cli/resolver.rs +++ b/packages/cli/binding/src/cli/resolver.rs @@ -67,10 +67,9 @@ impl SubcommandResolver { subcommand: SynthesizableSubcommand, resolved_vite_config: Option<&ResolvedUniversalViteConfig>, envs: &Arc, Arc>>, - cwd: &Arc, ) -> anyhow::Result { let command_name = subcommand.command_name(); - let mut resolved = self.resolve_inner(subcommand, resolved_vite_config, envs, cwd).await?; + let mut resolved = self.resolve_inner(subcommand, resolved_vite_config, envs).await?; // Inject VP_COMMAND so that defineConfig's plugin factory knows which command is running, // even when the subcommand is synthesized inside `vp run`. let envs = Arc::make_mut(&mut resolved.envs); @@ -83,7 +82,6 @@ impl SubcommandResolver { subcommand: SynthesizableSubcommand, resolved_vite_config: Option<&ResolvedUniversalViteConfig>, envs: &Arc, Arc>>, - cwd: &Arc, ) -> anyhow::Result { match subcommand { SynthesizableSubcommand::Lint { mut args } => { @@ -294,32 +292,6 @@ impl SubcommandResolver { "Check is a composite command and cannot be resolved to a single subcommand" ); } - SynthesizableSubcommand::Install { args } => { - let package_manager = - vite_install::PackageManager::builder(cwd).build_with_default().await?; - let resolve_command = package_manager.resolve_install_command(&args); - - let merged_envs = { - let mut env_map = FxHashMap::clone(envs); - for (k, v) in resolve_command.envs { - env_map.insert(Arc::from(OsStr::new(&k)), Arc::from(OsStr::new(&v))); - } - Arc::new(env_map) - }; - - Ok(ResolvedSubcommand { - program: Arc::::from( - OsStr::new(&resolve_command.bin_path).to_os_string(), - ), - args: resolve_command.args.into_iter().map(Str::from).collect(), - cache_config: UserCacheConfig::with_config(EnabledCacheConfig { - env: None, - untracked_env: None, - input: None, - }), - envs: merged_envs, - }) - } } } } diff --git a/packages/cli/binding/src/cli/types.rs b/packages/cli/binding/src/cli/types.rs index f17ca40509..4d9ae8147b 100644 --- a/packages/cli/binding/src/cli/types.rs +++ b/packages/cli/binding/src/cli/types.rs @@ -76,12 +76,6 @@ pub enum SynthesizableSubcommand { #[clap(allow_hyphen_values = true, trailing_var_arg = true)] args: Vec, }, - /// Install command. - #[command(disable_help_flag = true, alias = "i")] - Install { - #[clap(allow_hyphen_values = true, trailing_var_arg = true)] - args: Vec, - }, /// Run format, lint, and type checks Check { /// Auto-fix format and lint issues @@ -114,7 +108,6 @@ impl SynthesizableSubcommand { Self::Dev { .. } => "dev", Self::Preview { .. } => "preview", Self::Doc { .. } => "doc", - Self::Install { .. } => "install", Self::Check { .. } => "check", } } @@ -132,6 +125,10 @@ pub(super) enum CLIArgs { #[command(flatten)] Synthesizable(SynthesizableSubcommand), + /// Package manager commands (install, add, remove, update, dedupe, …) + #[command(flatten)] + PackageManager(vite_pm_cli::PackageManagerCommand), + /// Execute a command from local node_modules/.bin Exec(crate::exec::ExecArgs), } diff --git a/packages/cli/snap-tests/command-add-pnpm10/package.json b/packages/cli/snap-tests/command-add-pnpm10/package.json new file mode 100644 index 0000000000..4981001600 --- /dev/null +++ b/packages/cli/snap-tests/command-add-pnpm10/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-add-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@10.19.0" +} diff --git a/packages/cli/snap-tests/command-add-pnpm10/snap.txt b/packages/cli/snap-tests/command-add-pnpm10/snap.txt new file mode 100644 index 0000000000..3e50976eb5 --- /dev/null +++ b/packages/cli/snap-tests/command-add-pnpm10/snap.txt @@ -0,0 +1,161 @@ +> vp add --help # should show help +Add packages to dependencies + +Usage: vp add [OPTIONS] ... [-- ...] + +Arguments: + ... Packages to add + [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager + +Options: + -P, --save-prod + Save to `dependencies` (default) + -D, --save-dev + Save to `devDependencies` + --save-peer + Save to `peerDependencies` and `devDependencies` + -O, --save-optional + Save to `optionalDependencies` + -E, --save-exact + Save exact version rather than semver range + --save-catalog-name + Save the new dependency to the specified catalog name + --save-catalog + Save the new dependency to the default catalog + --allow-build + A list of package names allowed to run postinstall + --filter + Filter packages in monorepo (can be used multiple times) + -w, --workspace-root + Add to workspace root + --workspace + Only add if package exists in workspace (pnpm-specific) + -g, --global + Install globally + --node + Node.js version to use for global installation (only with -g) + -h, --help + Print help + +[2]> vp add # should error because no packages specified +error: the following required arguments were not provided: + ... + +Usage: vp add ... [-- ...] + +For more information, try '--help'. + +> vp add testnpm2 -D -- --loglevel=verbose --verbose && cat package.json # should add package as dev dependencies +Packages: + ++ +Progress: resolved , reused , downloaded , added , done + +devDependencies: ++ testnpm2 + +Done in ms using pnpm v +{ + "name": "command-add-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@", + "devDependencies": { + "testnpm2": "^1.0.1" + } +} + +> vp add testnpm2 test-vite-plus-install --allow-build=test-vite-plus-install && cat package.json # should add packages to dependencies +Packages: + ++ +Progress: resolved , reused , downloaded , added , done + +dependencies: ++ test-vite-plus-install + +Done in ms using pnpm v +{ + "name": "command-add-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@", + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "test-vite-plus-install": "^1.0.0" + } +} + +> vp install test-vite-plus-package@1.0.0 --save-peer && cat package.json # should install package alias for add +Packages: + ++ +Progress: resolved , reused , downloaded , added , done + +peerDependencies: ++ test-vite-plus-package + +devDependencies: ++ test-vite-plus-package already in devDependencies, was not moved to dependencies. + +Done in ms using pnpm v +{ + "name": "command-add-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@", + "devDependencies": { + "test-vite-plus-package": "1.0.0", + "testnpm2": "^1.0.1" + }, + "dependencies": { + "test-vite-plus-install": "^1.0.0" + }, + "peerDependencies": { + "test-vite-plus-package": "1.0.0" + } +} + +> vp add test-vite-plus-package-optional -O && cat package.json # should add package as optional dependencies +Packages: + ++ +Progress: resolved , reused , downloaded , added , done + +optionalDependencies: ++ test-vite-plus-package-optional + +Done in ms using pnpm v +{ + "name": "command-add-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@", + "devDependencies": { + "test-vite-plus-package": "1.0.0", + "testnpm2": "^1.0.1" + }, + "dependencies": { + "test-vite-plus-install": "^1.0.0" + }, + "peerDependencies": { + "test-vite-plus-package": "1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "^1.0.0" + } +} + +> vp add test-vite-plus-package-optional -- --loglevel=warn && cat package.json # support pass through arguments +{ + "name": "command-add-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@", + "devDependencies": { + "test-vite-plus-package": "1.0.0", + "testnpm2": "^1.0.1" + }, + "dependencies": { + "test-vite-plus-install": "^1.0.0" + }, + "peerDependencies": { + "test-vite-plus-package": "1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "^1.0.0" + } +} diff --git a/packages/cli/snap-tests/command-add-pnpm10/steps.json b/packages/cli/snap-tests/command-add-pnpm10/steps.json new file mode 100644 index 0000000000..2c0868694e --- /dev/null +++ b/packages/cli/snap-tests/command-add-pnpm10/steps.json @@ -0,0 +1,12 @@ +{ + "ignoredPlatforms": ["win32"], + "commands": [ + "vp add --help # should show help", + "vp add # should error because no packages specified", + "vp add testnpm2 -D -- --loglevel=verbose --verbose && cat package.json # should add package as dev dependencies", + "vp add testnpm2 test-vite-plus-install --allow-build=test-vite-plus-install && cat package.json # should add packages to dependencies", + "vp install test-vite-plus-package@1.0.0 --save-peer && cat package.json # should install package alias for add", + "vp add test-vite-plus-package-optional -O && cat package.json # should add package as optional dependencies", + "vp add test-vite-plus-package-optional -- --loglevel=warn && cat package.json # support pass through arguments" + ] +} diff --git a/packages/cli/snap-tests/command-dedupe-pnpm10/package.json b/packages/cli/snap-tests/command-dedupe-pnpm10/package.json new file mode 100644 index 0000000000..2d4616a507 --- /dev/null +++ b/packages/cli/snap-tests/command-dedupe-pnpm10/package.json @@ -0,0 +1,14 @@ +{ + "name": "command-dedupe-pnpm10", + "version": "1.0.0", + "dependencies": { + "testnpm2": "1.0.1" + }, + "devDependencies": { + "test-vite-plus-package": "1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "1.0.0" + }, + "packageManager": "pnpm@10.18.0" +} diff --git a/packages/cli/snap-tests/command-dedupe-pnpm10/snap.txt b/packages/cli/snap-tests/command-dedupe-pnpm10/snap.txt new file mode 100644 index 0000000000..93da8c01a5 --- /dev/null +++ b/packages/cli/snap-tests/command-dedupe-pnpm10/snap.txt @@ -0,0 +1,124 @@ +> vp dedupe --help # should show help +Deduplicate dependencies + +Usage: vp dedupe [OPTIONS] [-- ...] + +Arguments: + [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager + +Options: + --check Check if deduplication would make changes + -h, --help Print help + +> vp dedupe && cat package.json # should dedupe dependencies +Already up to date +Progress: resolved , reused , downloaded , added , done + +dependencies: ++ testnpm2 + +optionalDependencies: ++ test-vite-plus-package-optional + +devDependencies: ++ test-vite-plus-package + +{ + "name": "command-dedupe-pnpm10", + "version": "1.0.0", + "dependencies": { + "testnpm2": "1.0.1" + }, + "devDependencies": { + "test-vite-plus-package": "1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "1.0.0" + }, + "packageManager": "pnpm@" +} + +> vp dedupe --check && cat package.json # should check if deduplication would make changes +Progress: resolved , reused , downloaded , added , done + +{ + "name": "command-dedupe-pnpm10", + "version": "1.0.0", + "dependencies": { + "testnpm2": "1.0.1" + }, + "devDependencies": { + "test-vite-plus-package": "1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "1.0.0" + }, + "packageManager": "pnpm@" +} + +> vp dedupe -- --loglevel=warn && cat package.json # support pass through arguments +{ + "name": "command-dedupe-pnpm10", + "version": "1.0.0", + "dependencies": { + "testnpm2": "1.0.1" + }, + "devDependencies": { + "test-vite-plus-package": "1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "1.0.0" + }, + "packageManager": "pnpm@" +} + +[1]> json-edit package.json '_.dependencies = {}' && cat package.json && vp dedupe --check # should check fails because no dependencies +{ + "name": "command-dedupe-pnpm10", + "version": "1.0.0", + "dependencies": {}, + "devDependencies": { + "test-vite-plus-package": "1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "1.0.0" + }, + "packageManager": "pnpm@" +} +Progress: resolved , reused , downloaded , added , done + + ERR_PNPM_DEDUPE_CHECK_ISSUES  Dedupe --check found changes to the lockfile + +Importers +. +└── - testnpm2 + + +Packages +- testnpm2@ + +Run pnpm dedupe to apply the changes above. + + +> vp dedupe && cat package.json && vp dedupe --check # should dedupe fix the change by removing the dependencies +Packages: -1 +- +Progress: resolved , reused , downloaded , added , done + +dependencies: +- testnpm2 + +{ + "name": "command-dedupe-pnpm10", + "version": "1.0.0", + "dependencies": {}, + "devDependencies": { + "test-vite-plus-package": "1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "1.0.0" + }, + "packageManager": "pnpm@" +} +Progress: resolved , reused , downloaded , added , done + diff --git a/packages/cli/snap-tests/command-dedupe-pnpm10/steps.json b/packages/cli/snap-tests/command-dedupe-pnpm10/steps.json new file mode 100644 index 0000000000..6b161dfdc9 --- /dev/null +++ b/packages/cli/snap-tests/command-dedupe-pnpm10/steps.json @@ -0,0 +1,10 @@ +{ + "commands": [ + "vp dedupe --help # should show help", + "vp dedupe && cat package.json # should dedupe dependencies", + "vp dedupe --check && cat package.json # should check if deduplication would make changes", + "vp dedupe -- --loglevel=warn && cat package.json # support pass through arguments", + "json-edit package.json '_.dependencies = {}' && cat package.json && vp dedupe --check # should check fails because no dependencies", + "vp dedupe && cat package.json && vp dedupe --check # should dedupe fix the change by removing the dependencies" + ] +} diff --git a/packages/cli/snap-tests/command-dlx-pnpm10/package.json b/packages/cli/snap-tests/command-dlx-pnpm10/package.json new file mode 100644 index 0000000000..8f9b79b529 --- /dev/null +++ b/packages/cli/snap-tests/command-dlx-pnpm10/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-dlx-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@10.19.0" +} diff --git a/packages/cli/snap-tests/command-dlx-pnpm10/snap.txt b/packages/cli/snap-tests/command-dlx-pnpm10/snap.txt new file mode 100644 index 0000000000..fbfd1700bf --- /dev/null +++ b/packages/cli/snap-tests/command-dlx-pnpm10/snap.txt @@ -0,0 +1,33 @@ +> vp dlx --help # should show help message +Execute a package binary without installing it + +Usage: vp dlx [OPTIONS] ... + +Arguments: + ... Package to execute and arguments + +Options: + -p, --package Package(s) to install before running + -c, --shell-mode Execute within a shell environment + -s, --silent Suppress all output except the executed command's output + -h, --help Print help + +> vp dlx -s cowsay hello # should run cowsay with pnpm dlx + _______ +< hello > + ------- + \ ^__^ + \ (oo)\_______ + (__)\ )\/\ + ||----w | + || || + +> vp dlx -s cowsay@1.6.0 hello # should run specific version + _______ +< hello > + ------- + \ ^__^ + \ (oo)\_______ + (__)\ )\/\ + ||----w | + || || diff --git a/packages/cli/snap-tests/command-dlx-pnpm10/steps.json b/packages/cli/snap-tests/command-dlx-pnpm10/steps.json new file mode 100644 index 0000000000..8e2a8b772b --- /dev/null +++ b/packages/cli/snap-tests/command-dlx-pnpm10/steps.json @@ -0,0 +1,7 @@ +{ + "commands": [ + "vp dlx --help # should show help message", + "vp dlx -s cowsay hello # should run cowsay with pnpm dlx", + "vp dlx -s cowsay@1.6.0 hello # should run specific version" + ] +} diff --git a/packages/cli/snap-tests/command-info-no-package-json/snap.txt b/packages/cli/snap-tests/command-info-no-package-json/snap.txt new file mode 100644 index 0000000000..46fab479c8 --- /dev/null +++ b/packages/cli/snap-tests/command-info-no-package-json/snap.txt @@ -0,0 +1,5 @@ +> vp info vite@2.0.0 version # should work without package.json +2.0.0 + +> vp pm view vite@2.0.0 version # should work without package.json +2.0.0 diff --git a/packages/cli/snap-tests/command-info-no-package-json/steps.json b/packages/cli/snap-tests/command-info-no-package-json/steps.json new file mode 100644 index 0000000000..0594cbf220 --- /dev/null +++ b/packages/cli/snap-tests/command-info-no-package-json/steps.json @@ -0,0 +1,6 @@ +{ + "commands": [ + "vp info vite@2.0.0 version # should work without package.json", + "vp pm view vite@2.0.0 version # should work without package.json" + ] +} diff --git a/packages/cli/snap-tests/command-install-auto-create-package-json/snap.txt b/packages/cli/snap-tests/command-install-auto-create-package-json/snap.txt new file mode 100644 index 0000000000..eddce3053f --- /dev/null +++ b/packages/cli/snap-tests/command-install-auto-create-package-json/snap.txt @@ -0,0 +1,24 @@ +> test ! -f package.json && echo 'no package.json' # verify no package.json exists +no package.json + +> vp install --silent && cat package.json # should auto-create package.json and install +{ + "type": "module", + "packageManager": "pnpm@" +} +> vp add testnpm2 -D && cat package.json # should add package to auto-created package.json +Packages: + ++ +Progress: resolved , reused , downloaded , added , done + +devDependencies: ++ testnpm2 + +Done in ms using pnpm v +{ + "type": "module", + "packageManager": "pnpm@", + "devDependencies": { + "testnpm2": "^1.0.1" + } +} \ No newline at end of file diff --git a/packages/cli/snap-tests/command-install-auto-create-package-json/steps.json b/packages/cli/snap-tests/command-install-auto-create-package-json/steps.json new file mode 100644 index 0000000000..aa37f35ceb --- /dev/null +++ b/packages/cli/snap-tests/command-install-auto-create-package-json/steps.json @@ -0,0 +1,8 @@ +{ + "ignoredPlatforms": ["win32"], + "commands": [ + "test ! -f package.json && echo 'no package.json' # verify no package.json exists", + "vp install --silent && cat package.json # should auto-create package.json and install", + "vp add testnpm2 -D && cat package.json # should add package to auto-created package.json" + ] +} diff --git a/packages/cli/snap-tests/command-install-shortcut/snap.txt b/packages/cli/snap-tests/command-install-shortcut/snap.txt index 32cf648e41..3827ed4c46 100644 --- a/packages/cli/snap-tests/command-install-shortcut/snap.txt +++ b/packages/cli/snap-tests/command-install-shortcut/snap.txt @@ -1,5 +1,5 @@ > vp run install # install shortcut -$ vp install +$ vp install ⊘ cache disabled Packages: + + Progress: resolved , reused , downloaded , added , done @@ -9,15 +9,11 @@ dependencies: Done in ms using pnpm v ---- -vp run: command-install-shortcut#install not cached because it modified its input. (Run `vp run --last-details` for full details) > vp run install # install shortcut hit cache -$ vp install +$ vp install ⊘ cache disabled Lockfile is up to date, resolution step is skipped Already up to date Done in ms using pnpm v ---- -vp run: command-install-shortcut#install not cached because it modified its input. (Run `vp run --last-details` for full details) diff --git a/packages/cli/snap-tests/command-link-pnpm10/package.json b/packages/cli/snap-tests/command-link-pnpm10/package.json new file mode 100644 index 0000000000..e482469e5d --- /dev/null +++ b/packages/cli/snap-tests/command-link-pnpm10/package.json @@ -0,0 +1,8 @@ +{ + "name": "command-link-pnpm10", + "version": "1.0.0", + "dependencies": { + "testnpm2": "*" + }, + "packageManager": "pnpm@10.19.0" +} diff --git a/packages/cli/snap-tests/command-link-pnpm10/snap.txt b/packages/cli/snap-tests/command-link-pnpm10/snap.txt new file mode 100644 index 0000000000..95d2d38d00 --- /dev/null +++ b/packages/cli/snap-tests/command-link-pnpm10/snap.txt @@ -0,0 +1,111 @@ +> vp link -h # should show help message +Link packages for local development + +Usage: vp link [PACKAGE|DIR] [ARGS]... + +Arguments: + [PACKAGE|DIR] Package name or directory to link + [ARGS]... Arguments to pass to package manager + +Options: + -h, --help Print help + +> vp install # install initial dependencies +Packages: + ++ +Progress: resolved , reused , downloaded , added , done + +dependencies: ++ testnpm2 + +Done in ms using pnpm v + +> mkdir -p ../test-lib-pnpm && echo '{"name": "testnpm2", "version": "1.0.0"}' > ../test-lib-pnpm/package.json # create test library +> vp link ../test-lib-pnpm && cat package.json pnpm-lock.yaml # should link local directory +Packages: -1 +- + +dependencies: +- testnpm2 ++ testnpm2 <- ../test-lib-pnpm + +{ + "name": "command-link-pnpm10", + "version": "1.0.0", + "dependencies": { + "testnpm2": "*" + }, + "packageManager": "pnpm@" +} +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + testnpm2: link:../test-lib-pnpm + +importers: + + .: + dependencies: + testnpm2: + specifier: link:../test-lib-pnpm + version: link:../test-lib-pnpm + +> vp ln ../test-lib-pnpm && cat package.json pnpm-lock.yaml # should work with ln alias +Lockfile is up to date, resolution step is skipped + +{ + "name": "command-link-pnpm10", + "version": "1.0.0", + "dependencies": { + "testnpm2": "*" + }, + "packageManager": "pnpm@" +} +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + testnpm2: link:../test-lib-pnpm + +importers: + + .: + dependencies: + testnpm2: + specifier: link:../test-lib-pnpm + version: link:../test-lib-pnpm + +> vp unlink ../test-lib-pnpm && vp unlink testnpm2 && cat package.json pnpm-lock.yaml # should unlink the package +Nothing to unlink +Nothing to unlink +{ + "name": "command-link-pnpm10", + "version": "1.0.0", + "dependencies": { + "testnpm2": "*" + }, + "packageManager": "pnpm@" +} +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + testnpm2: link:../test-lib-pnpm + +importers: + + .: + dependencies: + testnpm2: + specifier: link:../test-lib-pnpm + version: link:../test-lib-pnpm diff --git a/packages/cli/snap-tests/command-link-pnpm10/steps.json b/packages/cli/snap-tests/command-link-pnpm10/steps.json new file mode 100644 index 0000000000..f56346977c --- /dev/null +++ b/packages/cli/snap-tests/command-link-pnpm10/steps.json @@ -0,0 +1,11 @@ +{ + "ignoredPlatforms": ["win32", "linux"], + "commands": [ + "vp link -h # should show help message", + "vp install # install initial dependencies", + "mkdir -p ../test-lib-pnpm && echo '{\"name\": \"testnpm2\", \"version\": \"1.0.0\"}' > ../test-lib-pnpm/package.json # create test library", + "vp link ../test-lib-pnpm && cat package.json pnpm-lock.yaml # should link local directory", + "vp ln ../test-lib-pnpm && cat package.json pnpm-lock.yaml # should work with ln alias", + "vp unlink ../test-lib-pnpm && vp unlink testnpm2 && cat package.json pnpm-lock.yaml # should unlink the package" + ] +} diff --git a/packages/cli/snap-tests/command-outdated-pnpm10/package.json b/packages/cli/snap-tests/command-outdated-pnpm10/package.json new file mode 100644 index 0000000000..74506a50e3 --- /dev/null +++ b/packages/cli/snap-tests/command-outdated-pnpm10/package.json @@ -0,0 +1,14 @@ +{ + "name": "command-outdated-pnpm10", + "version": "1.0.0", + "dependencies": { + "testnpm2": "1.0.0" + }, + "devDependencies": { + "test-vite-plus-top-package": "1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-other-optional": "1.0.0" + }, + "packageManager": "pnpm@10.18.0" +} diff --git a/packages/cli/snap-tests/command-outdated-pnpm10/snap.txt b/packages/cli/snap-tests/command-outdated-pnpm10/snap.txt new file mode 100644 index 0000000000..e3dd24cf81 --- /dev/null +++ b/packages/cli/snap-tests/command-outdated-pnpm10/snap.txt @@ -0,0 +1,173 @@ +> vp outdated --help # should show help +Check for outdated packages + +Usage: vp outdated [OPTIONS] [PACKAGES]... [-- ...] + +Arguments: + [PACKAGES]... Package name(s) to check + [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager + +Options: + --long Show extended information + --format Output format: table (default), list, or json + -r, --recursive Check recursively across all workspaces + --filter Filter packages in monorepo + -w, --workspace-root Include workspace root + -P, --prod Only production and optional dependencies + -D, --dev Only dev dependencies + --no-optional Exclude optional dependencies + --compatible Only show compatible versions + --sort-by Sort results by field + -g, --global Check globally installed packages + -h, --help Print help + +> vp install # should install packages first +Packages: + ++ +Progress: resolved , reused , downloaded , added , done + +dependencies: ++ testnpm2 (1.0.1 is available) + +optionalDependencies: ++ test-vite-plus-other-optional (1.1.0 is available) + +devDependencies: ++ test-vite-plus-top-package (1.1.0 is available) + +Done in ms using pnpm v + +[1]> vp outdated testnpm2 # should outdated package +┌──────────┬─────────┬────────┐ +│ Package │ Current │ Latest │ +├──────────┼─────────┼────────┤ +│ testnpm2 │ │ +└──────────┴─────────┴────────┘ + +[1]> vp outdated test-vite* # should outdated with one glob pattern +┌──────────────────────────────────────────┬─────────┬────────┐ +│ Package │ Current │ Latest │ +├──────────────────────────────────────────┼─────────┼────────┤ +│ test-vite-plus-other-optional (optional) │ │ +├──────────────────────────────────────────┼─────────┼────────┤ +│ test-vite-plus-top-package (dev) │ │ +└──────────────────────────────────────────┴─────────┴────────┘ + +[1]> vp outdated test-vite* '*npm*' # should outdated with multiple glob patterns +┌──────────────────────────────────────────┬─────────┬────────┐ +│ Package │ Current │ Latest │ +├──────────────────────────────────────────┼─────────┼────────┤ +│ testnpm2 │ │ +├──────────────────────────────────────────┼─────────┼────────┤ +│ test-vite-plus-other-optional (optional) │ │ +├──────────────────────────────────────────┼─────────┼────────┤ +│ test-vite-plus-top-package (dev) │ │ +└──────────────────────────────────────────┴─────────┴────────┘ + +[1]> vp outdated --format json # should support json output +{ + "testnpm2": { + "current": "1.0.0", + "latest": "1.0.1", + "wanted": "1.0.0", + "isDeprecated": false, + "dependencyType": "dependencies" + }, + "test-vite-plus-other-optional": { + "current": "1.0.0", + "latest": "1.1.0", + "wanted": "1.0.0", + "isDeprecated": false, + "dependencyType": "optionalDependencies" + }, + "test-vite-plus-top-package": { + "current": "1.0.0", + "latest": "1.1.0", + "wanted": "1.0.0", + "isDeprecated": false, + "dependencyType": "devDependencies" + } +} + +[1]> vp outdated --format list # should support list output +testnpm2 + => + +test-vite-plus-other-optional (optional) + => + +test-vite-plus-top-package (dev) + => + +[1]> vp outdated --format table # should support table output +┌──────────────────────────────────────────┬─────────┬────────┐ +│ Package │ Current │ Latest │ +├──────────────────────────────────────────┼─────────┼────────┤ +│ testnpm2 │ │ +├──────────────────────────────────────────┼─────────┼────────┤ +│ test-vite-plus-other-optional (optional) │ │ +├──────────────────────────────────────────┼─────────┼────────┤ +│ test-vite-plus-top-package (dev) │ │ +└──────────────────────────────────────────┴─────────┴────────┘ + +[1]> vp outdated testnpm2 --long --format list # should support --long +testnpm2 + => + +[1]> vp outdated -r # should support recursive output +┌──────────────────────────────────────────┬─────────┬────────┬─────────────────────────┐ +│ Package │ Current │ Latest │ Dependents │ +├──────────────────────────────────────────┼─────────┼────────┼─────────────────────────┤ +│ testnpm2 │ │ command-outdated-pnpm10 │ +├──────────────────────────────────────────┼─────────┼────────┼─────────────────────────┤ +│ test-vite-plus-other-optional (optional) │ │ command-outdated-pnpm10 │ +├──────────────────────────────────────────┼─────────┼────────┼─────────────────────────┤ +│ test-vite-plus-top-package (dev) │ │ command-outdated-pnpm10 │ +└──────────────────────────────────────────┴─────────┴────────┴─────────────────────────┘ + +[1]> vp outdated -P # should support prod output +┌──────────────────────────────────────────┬─────────┬────────┐ +│ Package │ Current │ Latest │ +├──────────────────────────────────────────┼─────────┼────────┤ +│ testnpm2 │ │ +├──────────────────────────────────────────┼─────────┼────────┤ +│ test-vite-plus-other-optional (optional) │ │ +└──────────────────────────────────────────┴─────────┴────────┘ + +[1]> vp outdated -D # should support dev output +┌──────────────────────────────────┬─────────┬────────┐ +│ Package │ Current │ Latest │ +├──────────────────────────────────┼─────────┼────────┤ +│ test-vite-plus-top-package (dev) │ │ +└──────────────────────────────────┴─────────┴────────┘ + +[1]> vp outdated --no-optional # should support no-optional output +┌──────────────────────────────────┬─────────┬────────┐ +│ Package │ Current │ Latest │ +├──────────────────────────────────┼─────────┼────────┤ +│ testnpm2 │ │ +├──────────────────────────────────┼─────────┼────────┤ +│ test-vite-plus-top-package (dev) │ │ +└──────────────────────────────────┴─────────┴────────┘ + +> vp outdated --compatible # should compatible output nothing +[1]> json-edit package.json '_.optionalDependencies["test-vite-plus-other-optional"] = "^1.0.0"' && vp outdated --compatible # should support compatible output with optional dependencies +┌──────────────────────────────────────────┬─────────┬────────┐ +│ Package │ Current │ Latest │ +├──────────────────────────────────────────┼─────────┼────────┤ +│ test-vite-plus-other-optional (optional) │ │ +└──────────────────────────────────────────┴─────────┴────────┘ + +[1]> vp outdated --sort-by name # should support sort-by output +┌──────────────────────────────────────────┬─────────┬────────┐ +│ Package │ Current │ Latest │ +├──────────────────────────────────────────┼─────────┼────────┤ +│ test-vite-plus-other-optional (optional) │ │ +├──────────────────────────────────────────┼─────────┼────────┤ +│ test-vite-plus-top-package (dev) │ │ +├──────────────────────────────────────────┼─────────┼────────┤ +│ testnpm2 │ │ +└──────────────────────────────────────────┴─────────┴────────┘ + +> vp outdated testnpm2 -g --format json # should support global output +{} diff --git a/packages/cli/snap-tests/command-outdated-pnpm10/steps.json b/packages/cli/snap-tests/command-outdated-pnpm10/steps.json new file mode 100644 index 0000000000..b1e9e87cf3 --- /dev/null +++ b/packages/cli/snap-tests/command-outdated-pnpm10/steps.json @@ -0,0 +1,22 @@ +{ + "ignoredPlatforms": ["win32"], + "commands": [ + "vp outdated --help # should show help", + "vp install # should install packages first", + "vp outdated testnpm2 # should outdated package", + "vp outdated test-vite* # should outdated with one glob pattern", + "vp outdated test-vite* '*npm*' # should outdated with multiple glob patterns", + "vp outdated --format json # should support json output", + "vp outdated --format list # should support list output", + "vp outdated --format table # should support table output", + "vp outdated testnpm2 --long --format list # should support --long", + "vp outdated -r # should support recursive output", + "vp outdated -P # should support prod output", + "vp outdated -D # should support dev output", + "vp outdated --no-optional # should support no-optional output", + "vp outdated --compatible # should compatible output nothing", + "json-edit package.json '_.optionalDependencies[\"test-vite-plus-other-optional\"] = \"^1.0.0\"' && vp outdated --compatible # should support compatible output with optional dependencies", + "vp outdated --sort-by name # should support sort-by output", + "vp outdated testnpm2 -g --format json # should support global output" + ] +} diff --git a/packages/cli/snap-tests/command-pm-global-rejected/package.json b/packages/cli/snap-tests/command-pm-global-rejected/package.json new file mode 100644 index 0000000000..0d9832b5db --- /dev/null +++ b/packages/cli/snap-tests/command-pm-global-rejected/package.json @@ -0,0 +1,6 @@ +{ + "name": "command-pm-global-rejected", + "version": "1.0.0", + "private": true, + "packageManager": "pnpm@10.15.1" +} diff --git a/packages/cli/snap-tests/command-pm-global-rejected/snap.txt b/packages/cli/snap-tests/command-pm-global-rejected/snap.txt new file mode 100644 index 0000000000..4054af9d03 --- /dev/null +++ b/packages/cli/snap-tests/command-pm-global-rejected/snap.txt @@ -0,0 +1,26 @@ +[1]> vp install -g testnpm2 # rejected: managed install +error: 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. + +[1]> vp install -g testnpm2 --node 20 # rejected: --node implicitly covered +error: 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. + +[1]> vp add -g testnpm2 # rejected: managed add +error: 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. + +[1]> vp add -g testnpm2 --node 20 # rejected: --node implicitly covered +error: 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. + +[1]> vp remove -g testnpm2 # rejected: managed uninstall +error: 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. + +[1]> vp remove -g --dry-run testnpm2 # rejected: --dry-run implicitly covered +error: 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. + +[1]> vp update -g # rejected: managed update +error: 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. + +[1]> vp update -g testnpm2 # rejected: managed update with package +error: 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. + +[1]> vp pm ls -g # rejected: managed packages listing +error: 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. diff --git a/packages/cli/snap-tests/command-pm-global-rejected/steps.json b/packages/cli/snap-tests/command-pm-global-rejected/steps.json new file mode 100644 index 0000000000..857264833c --- /dev/null +++ b/packages/cli/snap-tests/command-pm-global-rejected/steps.json @@ -0,0 +1,13 @@ +{ + "commands": [ + "vp install -g testnpm2 # rejected: managed install", + "vp install -g testnpm2 --node 20 # rejected: --node implicitly covered", + "vp add -g testnpm2 # rejected: managed add", + "vp add -g testnpm2 --node 20 # rejected: --node implicitly covered", + "vp remove -g testnpm2 # rejected: managed uninstall", + "vp remove -g --dry-run testnpm2 # rejected: --dry-run implicitly covered", + "vp update -g # rejected: managed update", + "vp update -g testnpm2 # rejected: managed update with package", + "vp pm ls -g # rejected: managed packages listing" + ] +} diff --git a/packages/cli/snap-tests/command-pm-no-package-json/snap.txt b/packages/cli/snap-tests/command-pm-no-package-json/snap.txt new file mode 100644 index 0000000000..2707e25d5e --- /dev/null +++ b/packages/cli/snap-tests/command-pm-no-package-json/snap.txt @@ -0,0 +1,11 @@ +[1]> vp pm ls # should show friendly error +error: No package.json found. + +[1]> vp pm prune # should show friendly error +error: No package.json found. + +[1]> vp outdated # should show friendly error +error: No package.json found. + +[1]> vp why lodash # should show friendly error +error: No package.json found. diff --git a/packages/cli/snap-tests/command-pm-no-package-json/steps.json b/packages/cli/snap-tests/command-pm-no-package-json/steps.json new file mode 100644 index 0000000000..4710180dbb --- /dev/null +++ b/packages/cli/snap-tests/command-pm-no-package-json/steps.json @@ -0,0 +1,8 @@ +{ + "commands": [ + "vp pm ls # should show friendly error", + "vp pm prune # should show friendly error", + "vp outdated # should show friendly error", + "vp why lodash # should show friendly error" + ] +} diff --git a/packages/cli/snap-tests/command-remove-pnpm10/package.json b/packages/cli/snap-tests/command-remove-pnpm10/package.json new file mode 100644 index 0000000000..b0f25d1d6c --- /dev/null +++ b/packages/cli/snap-tests/command-remove-pnpm10/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-remove-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@10.18.0" +} diff --git a/packages/cli/snap-tests/command-remove-pnpm10/snap.txt b/packages/cli/snap-tests/command-remove-pnpm10/snap.txt new file mode 100644 index 0000000000..dda994ebc2 --- /dev/null +++ b/packages/cli/snap-tests/command-remove-pnpm10/snap.txt @@ -0,0 +1,106 @@ +> vp remove --help # should show help +Remove packages from dependencies + +Usage: vp remove [OPTIONS] ... [-- ...] + +Arguments: + ... Packages to remove + [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager + +Options: + -D, --save-dev Only remove from `devDependencies` (pnpm-specific) + -O, --save-optional Only remove from `optionalDependencies` (pnpm-specific) + -P, --save-prod Only remove from `dependencies` (pnpm-specific) + --filter Filter packages in monorepo (can be used multiple times) + -w, --workspace-root Remove from workspace root + -r, --recursive Remove recursively from all workspace packages + -g, --global Remove global packages + --dry-run Preview what would be removed without actually removing (only with -g) + -h, --help Print help + +[2]> vp remove # should error because no packages specified +error: the following required arguments were not provided: + ... + +Usage: vp remove ... [-- ...] + +For more information, try '--help'. + +[1]> vp remove testnpm2 -D && cat package.json # should error when remove not exists package from dev dependencies + ERR_PNPM_CANNOT_REMOVE_MISSING_DEPS  Cannot remove 'testnpm2': project has no 'devDependencies' + +> vp add testnpm2 && vp add -D test-vite-plus-install && vp add -O test-vite-plus-package-optional && cat package.json # should add packages to dependencies +Packages: + ++ +Progress: resolved , reused , downloaded , added , done + +dependencies: ++ testnpm2 + +Done in ms using pnpm v +Packages: + ++ +Progress: resolved , reused , downloaded , added , done + +devDependencies: ++ test-vite-plus-install + +Done in ms using pnpm v +Packages: + ++ +Progress: resolved , reused , downloaded , added , done + +optionalDependencies: ++ test-vite-plus-package-optional + +Done in ms using pnpm v +{ + "name": "command-remove-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@", + "dependencies": { + "testnpm2": "^1.0.1" + }, + "devDependencies": { + "test-vite-plus-install": "^1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "^1.0.0" + } +} + +> vp remove testnpm2 test-vite-plus-install && cat package.json # should remove packages from dependencies +Packages: -2 +-- +Progress: resolved , reused , downloaded , added , done + +dependencies: +- testnpm2 + +devDependencies: +- test-vite-plus-install + +Done in ms using pnpm v +{ + "name": "command-remove-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@", + "optionalDependencies": { + "test-vite-plus-package-optional": "^1.0.0" + } +} + +> vp remove -O test-vite-plus-package-optional -- --loglevel=warn && cat package.json # support remove package from optional dependencies and pass through arguments +{ + "name": "command-remove-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@" +} + +[1]> vp remove -g --dry-run testnpm2 && cat package.json # support remove global package with dry-run +error: 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. + +[2]> vp rm --stream foo && should show tips to use pass through arguments when options are not supported +error: Unexpected argument '--stream' + +Use `-- --stream` to pass the argument as a value diff --git a/packages/cli/snap-tests/command-remove-pnpm10/steps.json b/packages/cli/snap-tests/command-remove-pnpm10/steps.json new file mode 100644 index 0000000000..7a98a5e6b4 --- /dev/null +++ b/packages/cli/snap-tests/command-remove-pnpm10/steps.json @@ -0,0 +1,13 @@ +{ + "ignoredPlatforms": ["win32"], + "commands": [ + "vp remove --help # should show help", + "vp remove # should error because no packages specified", + "vp remove testnpm2 -D && cat package.json # should error when remove not exists package from dev dependencies", + "vp add testnpm2 && vp add -D test-vite-plus-install && vp add -O test-vite-plus-package-optional && cat package.json # should add packages to dependencies", + "vp remove testnpm2 test-vite-plus-install && cat package.json # should remove packages from dependencies", + "vp remove -O test-vite-plus-package-optional -- --loglevel=warn && cat package.json # support remove package from optional dependencies and pass through arguments", + "vp remove -g --dry-run testnpm2 && cat package.json # support remove global package with dry-run", + "vp rm --stream foo && should show tips to use pass through arguments when options are not supported" + ] +} diff --git a/packages/cli/snap-tests/command-unlink-pnpm10/package.json b/packages/cli/snap-tests/command-unlink-pnpm10/package.json new file mode 100644 index 0000000000..d95db35421 --- /dev/null +++ b/packages/cli/snap-tests/command-unlink-pnpm10/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-unlink-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@10.19.0" +} diff --git a/packages/cli/snap-tests/command-unlink-pnpm10/snap.txt b/packages/cli/snap-tests/command-unlink-pnpm10/snap.txt new file mode 100644 index 0000000000..09b61e2d49 --- /dev/null +++ b/packages/cli/snap-tests/command-unlink-pnpm10/snap.txt @@ -0,0 +1,53 @@ +> vp unlink -h # should show help message +Unlink packages + +Usage: vp unlink [OPTIONS] [PACKAGE|DIR] [ARGS]... + +Arguments: + [PACKAGE|DIR] Package name to unlink + [ARGS]... Arguments to pass to package manager + +Options: + -r, --recursive Unlink in every workspace package + -h, --help Print help + +> mkdir -p ../unlink-test-lib && echo '{"name": "unlink-test-lib", "version": "1.0.0"}' > ../unlink-test-lib/package.json # create test library +> vp link ../unlink-test-lib && cat package.json # link the library first + +dependencies: ++ unlink-test-lib <- ../unlink-test-lib + +{ + "name": "command-unlink-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@", + "dependencies": { + "unlink-test-lib": "link:../unlink-test-lib" + } +} + +> vp unlink unlink-test-lib && cat package.json # should unlink the package +Nothing to unlink +{ + "name": "command-unlink-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@", + "dependencies": { + "unlink-test-lib": "link:../unlink-test-lib" + } +} + +> vp link ../unlink-test-lib # link again +Lockfile is up to date, resolution step is skipped + + +> vp unlink && cat package.json # should unlink all packages +Nothing to unlink +{ + "name": "command-unlink-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@", + "dependencies": { + "unlink-test-lib": "link:../unlink-test-lib" + } +} diff --git a/packages/cli/snap-tests/command-unlink-pnpm10/steps.json b/packages/cli/snap-tests/command-unlink-pnpm10/steps.json new file mode 100644 index 0000000000..491581a67b --- /dev/null +++ b/packages/cli/snap-tests/command-unlink-pnpm10/steps.json @@ -0,0 +1,11 @@ +{ + "ignoredPlatforms": ["win32"], + "commands": [ + "vp unlink -h # should show help message", + "mkdir -p ../unlink-test-lib && echo '{\"name\": \"unlink-test-lib\", \"version\": \"1.0.0\"}' > ../unlink-test-lib/package.json # create test library", + "vp link ../unlink-test-lib && cat package.json # link the library first", + "vp unlink unlink-test-lib && cat package.json # should unlink the package", + "vp link ../unlink-test-lib # link again", + "vp unlink && cat package.json # should unlink all packages" + ] +} diff --git a/packages/cli/snap-tests/command-update-pnpm10/package.json b/packages/cli/snap-tests/command-update-pnpm10/package.json new file mode 100644 index 0000000000..382f5a7d02 --- /dev/null +++ b/packages/cli/snap-tests/command-update-pnpm10/package.json @@ -0,0 +1,14 @@ +{ + "name": "command-update-pnpm10", + "version": "1.0.0", + "dependencies": { + "testnpm2": "*" + }, + "devDependencies": { + "test-vite-plus-package": "*" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "*" + }, + "packageManager": "pnpm@10.18.0" +} diff --git a/packages/cli/snap-tests/command-update-pnpm10/snap.txt b/packages/cli/snap-tests/command-update-pnpm10/snap.txt new file mode 100644 index 0000000000..65d899346f --- /dev/null +++ b/packages/cli/snap-tests/command-update-pnpm10/snap.txt @@ -0,0 +1,175 @@ +> vp update --help # should show help +Update packages to their latest versions + +Usage: vp update [OPTIONS] [PACKAGES]... [-- ...] + +Arguments: + [PACKAGES]... Packages to update (optional - updates all if omitted) + [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager + +Options: + -L, --latest Update to latest version (ignore semver range) + -g, --global Update global packages + -r, --recursive Update recursively in all workspace packages + --filter Filter packages in monorepo (can be used multiple times) + -w, --workspace-root Include workspace root + -D, --dev Update only devDependencies + -P, --prod Update only dependencies (production) + -i, --interactive Interactive mode + --no-optional Don't update optionalDependencies + --no-save Update lockfile only, don't modify package.json + --workspace Only update if package exists in workspace (pnpm-specific) + -h, --help Print help + +> vp update testnpm2 && cat package.json # should update package within semver range +Packages: + ++ +Progress: resolved , reused , downloaded , added , done + +dependencies: ++ testnpm2 + +optionalDependencies: ++ test-vite-plus-package-optional + +devDependencies: ++ test-vite-plus-package + +Done in ms using pnpm v +{ + "name": "command-update-pnpm10", + "version": "1.0.0", + "dependencies": { + "testnpm2": "^1.0.1" + }, + "devDependencies": { + "test-vite-plus-package": "*" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "*" + }, + "packageManager": "pnpm@" +} + +> vp up testnpm2 --latest && cat package.json # should to absolute latest version +Already up to date +Progress: resolved , reused , downloaded , added , done + +Done in ms using pnpm v +{ + "name": "command-update-pnpm10", + "version": "1.0.0", + "dependencies": { + "testnpm2": "^1.0.1" + }, + "devDependencies": { + "test-vite-plus-package": "*" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "*" + }, + "packageManager": "pnpm@" +} + +> vp update -D && cat package.json # should update only dev dependencies +Already up to date +Progress: resolved , reused , downloaded , added , done + +dependencies: skipped + +optionalDependencies: skipped + +Done in ms using pnpm v +{ + "name": "command-update-pnpm10", + "version": "1.0.0", + "dependencies": { + "testnpm2": "^1.0.1" + }, + "devDependencies": { + "test-vite-plus-package": "^1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "*" + }, + "packageManager": "pnpm@" +} + +> vp update -P --no-save && cat package.json # should update only dependencies and optionalDependencies without saving +Already up to date +Progress: resolved , reused , downloaded , added , done + +devDependencies: skipped + +Done in ms using pnpm v +{ + "name": "command-update-pnpm10", + "version": "1.0.0", + "dependencies": { + "testnpm2": "^1.0.1" + }, + "devDependencies": { + "test-vite-plus-package": "^1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "*" + }, + "packageManager": "pnpm@" +} + +> vp rm testnpm2 # should remove package from dependencies for the next test +> vp add testnpm2@1.0.0 -O && vp update --no-optional --latest && cat package.json # should skip optional dependencies +Packages: + ++ +Progress: resolved , reused , downloaded , added , done + +optionalDependencies: ++ testnpm2 (1.0.1 is available) + +Done in ms using pnpm v +Packages: -2 +-- +Progress: resolved , reused , downloaded , added , done + +optionalDependencies: +- test-vite-plus-package-optional +- testnpm2 + +Done in ms using pnpm v +{ + "name": "command-update-pnpm10", + "version": "1.0.0", + "devDependencies": { + "test-vite-plus-package": "^1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "^1.0.0", + "testnpm2": "1.0.1" + }, + "packageManager": "pnpm@" +} + +> vp update && vp update --recursive && cat package.json # should update all packages and change the package.json +Packages: + ++ +Progress: resolved , reused , downloaded , added , done + +optionalDependencies: ++ test-vite-plus-package-optional ++ testnpm2 + +Done in ms using pnpm v +Progress: resolved , reused , downloaded , added , done +Done in ms using pnpm v +{ + "name": "command-update-pnpm10", + "version": "1.0.0", + "devDependencies": { + "test-vite-plus-package": "^1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "^1.0.0", + "testnpm2": "1.0.1" + }, + "packageManager": "pnpm@" +} diff --git a/packages/cli/snap-tests/command-update-pnpm10/steps.json b/packages/cli/snap-tests/command-update-pnpm10/steps.json new file mode 100644 index 0000000000..6e517683eb --- /dev/null +++ b/packages/cli/snap-tests/command-update-pnpm10/steps.json @@ -0,0 +1,16 @@ +{ + "ignoredPlatforms": ["win32"], + "commands": [ + "vp update --help # should show help", + "vp update testnpm2 && cat package.json # should update package within semver range", + "vp up testnpm2 --latest && cat package.json # should to absolute latest version", + "vp update -D && cat package.json # should update only dev dependencies", + "vp update -P --no-save && cat package.json # should update only dependencies and optionalDependencies without saving", + { + "command": "vp rm testnpm2 # should remove package from dependencies for the next test", + "ignoreOutput": true + }, + "vp add testnpm2@1.0.0 -O && vp update --no-optional --latest && cat package.json # should skip optional dependencies", + "vp update && vp update --recursive && cat package.json # should update all packages and change the package.json" + ] +} diff --git a/packages/cli/snap-tests/command-why-pnpm10/package.json b/packages/cli/snap-tests/command-why-pnpm10/package.json new file mode 100644 index 0000000000..ef6706d2ee --- /dev/null +++ b/packages/cli/snap-tests/command-why-pnpm10/package.json @@ -0,0 +1,14 @@ +{ + "name": "command-why-pnpm10", + "version": "1.0.0", + "dependencies": { + "testnpm2": "1.0.1" + }, + "devDependencies": { + "test-vite-plus-package": "1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "1.0.0" + }, + "packageManager": "pnpm@10.18.0" +} diff --git a/packages/cli/snap-tests/command-why-pnpm10/snap.txt b/packages/cli/snap-tests/command-why-pnpm10/snap.txt new file mode 100644 index 0000000000..8c8e6baee4 --- /dev/null +++ b/packages/cli/snap-tests/command-why-pnpm10/snap.txt @@ -0,0 +1,142 @@ +> vp why --help # should show help +Show why a package is installed + +Usage: vp why [OPTIONS] ... [-- ...] + +Arguments: + ... Package(s) to check + [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager + +Options: + --json Output in JSON format + --long Show extended information + --parseable Show parseable output + -r, --recursive Check recursively across all workspaces + --filter Filter packages in monorepo + -w, --workspace-root Check in workspace root + -P, --prod Only production dependencies + -D, --dev Only dev dependencies + --depth Limit tree depth + --no-optional Exclude optional dependencies + -g, --global Check globally installed packages + --exclude-peers Exclude peer dependencies + --find-by Use a finder function defined in .pnpmfile.cjs + -h, --help Print help + +> vp install # should install packages first +Packages: + ++ +Progress: resolved , reused , downloaded , added , done + +dependencies: ++ testnpm2 + +optionalDependencies: ++ test-vite-plus-package-optional + +devDependencies: ++ test-vite-plus-package + +Done in ms using pnpm v + +> vp why testnpm2 # should show why package is installed +Legend: production dependency, optional only, dev only + +command-why-pnpm10@ + +dependencies: +testnpm2 + +> vp explain testnpm2 # should work with explain alias +Legend: production dependency, optional only, dev only + +command-why-pnpm10@ + +dependencies: +testnpm2 + +> vp why test-vite-plus-package # should show why dev package is installed +Legend: production dependency, optional only, dev only + +command-why-pnpm10@ + +devDependencies: +test-vite-plus-package + +> vp why testnpm2 test-vite-plus-package # should support multiple packages +Legend: production dependency, optional only, dev only + +command-why-pnpm10@ + +dependencies: +testnpm2 + +devDependencies: +test-vite-plus-package + +> vp why testnpm2 --json # should support json output +[ + { + "name": "command-why-pnpm10", + "version": "1.0.0", + "path": "", + "private": false, + "dependencies": { + "testnpm2": { + "from": "testnpm2", + "version": "1.0.1", + "resolved": "https://registry./testnpm2/-/testnpm2-1.0.1.tgz", + "path": "/node_modules/.pnpm/testnpm2@/node_modules/testnpm2" + } + } + } +] + +> vp why testnpm2 --long # should support long output +Legend: production dependency, optional only, dev only + +command-why-pnpm10@ + +dependencies: +testnpm2 + /node_modules/.pnpm/testnpm2@/node_modules/testnpm2 + +> vp why testnpm2 --parseable # should support parseable output + +/node_modules/.pnpm/testnpm2@/node_modules/testnpm2 + +> vp why testnpm2 -P # should support prod dependencies only +Legend: production dependency, optional only, dev only + +command-why-pnpm10@ + +dependencies: +testnpm2 + +> vp why test-vite-plus-package -D # should support dev dependencies only +Legend: production dependency, optional only, dev only + +command-why-pnpm10@ + +devDependencies: +test-vite-plus-package + +> vp why testnpm2 --depth 1 # should support depth limiting +Legend: production dependency, optional only, dev only + +command-why-pnpm10@ + +dependencies: +testnpm2 + +> vp why test-vite-plus-package-optional --no-optional # should exclude optional dependencies +[1]> vp why testnpm2 --find-by customFinder # should support find-by option (pnpm-specific) + ERR_PNPM_FINDER_NOT_FOUND  No finder with name customFinder is found + +> vp why testnpm2 -- --reporter=silent # should support pass through arguments +Legend: production dependency, optional only, dev only + +command-why-pnpm10@ + +dependencies: +testnpm2 diff --git a/packages/cli/snap-tests/command-why-pnpm10/steps.json b/packages/cli/snap-tests/command-why-pnpm10/steps.json new file mode 100644 index 0000000000..e7b38a4206 --- /dev/null +++ b/packages/cli/snap-tests/command-why-pnpm10/steps.json @@ -0,0 +1,20 @@ +{ + "ignoredPlatforms": ["win32"], + "commands": [ + "vp why --help # should show help", + "vp install # should install packages first", + "vp why testnpm2 # should show why package is installed", + "vp explain testnpm2 # should work with explain alias", + "vp why test-vite-plus-package # should show why dev package is installed", + "vp why testnpm2 test-vite-plus-package # should support multiple packages", + "vp why testnpm2 --json # should support json output", + "vp why testnpm2 --long # should support long output", + "vp why testnpm2 --parseable # should support parseable output", + "vp why testnpm2 -P # should support prod dependencies only", + "vp why test-vite-plus-package -D # should support dev dependencies only", + "vp why testnpm2 --depth 1 # should support depth limiting", + "vp why test-vite-plus-package-optional --no-optional # should exclude optional dependencies", + "vp why testnpm2 --find-by customFinder # should support find-by option (pnpm-specific)", + "vp why testnpm2 -- --reporter=silent # should support pass through arguments" + ] +} diff --git a/packages/cli/snap-tests/npm-install-with-options/snap.txt b/packages/cli/snap-tests/npm-install-with-options/snap.txt index e8ee1c0267..c16d851f41 100644 --- a/packages/cli/snap-tests/npm-install-with-options/snap.txt +++ b/packages/cli/snap-tests/npm-install-with-options/snap.txt @@ -1,27 +1,40 @@ > vp install --help # print help message -Install a package +Install all dependencies, or add packages if package names are provided -Usage: -npm install [ ...] +Usage: vp install [OPTIONS] [PACKAGES]... [-- ...] -Options: -[-S|--save|--no-save|--save-prod|--save-dev|--save-optional|--save-peer|--save-bundle] -[-E|--save-exact] [-g|--global] -[--install-strategy ] [--legacy-bundling] -[--global-style] [--omit [--omit ...]] -[--include [--include ...]] -[--strict-peer-deps] [--prefer-dedupe] [--no-package-lock] [--package-lock-only] -[--foreground-scripts] [--ignore-scripts] [--no-audit] [--no-bin-links] -[--no-fund] [--dry-run] [--cpu ] [--os ] [--libc ] -[-w|--workspace [-w|--workspace ...]] -[-ws|--workspaces] [--include-workspace-root] [--install-links] - -aliases: add, i, in, ins, inst, insta, instal, isnt, isnta, isntal, isntall +Arguments: + [PACKAGES]... Packages to add (if provided, acts as `vp add`) + [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager -Run "npm help install" for more info +Options: + -P, --prod Do not install devDependencies + -D, --dev Only install devDependencies (install) / Save to devDependencies (add) + --no-optional Do not install optionalDependencies + --frozen-lockfile Fail if lockfile needs to be updated (CI mode) + --no-frozen-lockfile Allow lockfile updates (opposite of --frozen-lockfile) + --lockfile-only Only update lockfile, don't install + --prefer-offline Use cached packages when available + --offline Only use packages already in cache + -f, --force Force reinstall all dependencies + --ignore-scripts Do not run lifecycle scripts + --no-lockfile Don't read or generate lockfile + --fix-lockfile Fix broken lockfile entries (pnpm and yarn@2+ only) + --shamefully-hoist Create flat `node_modules` (pnpm only) + --resolution-only Re-run resolution for peer dependency analysis (pnpm only) + --silent Suppress output (silent mode) + --filter Filter packages in monorepo (can be used multiple times) + -w, --workspace-root Install in workspace root only + -E, --save-exact Save exact version (only when adding packages) + --save-peer Save to peerDependencies (only when adding packages) + -O, --save-optional Save to optionalDependencies (only when adding packages) + --save-catalog Save the new dependency to the default catalog (only when adding packages) + -g, --global Install globally (requires package names) + --node Node.js version to use for global installation (only with -g) + -h, --help Print help > vp run install # https://docs.npmjs.com/cli/v10/commands/npm-install -$ vp install --production --silent +$ vp install --prod --silent ⊘ cache disabled > ls node_modules @@ -29,10 +42,8 @@ $ vp install --production --silent tslib > vp run install # install again hit cache -$ vp install --production --silent ◉ cache hit, replaying +$ vp install --prod --silent ⊘ cache disabled ---- -vp run: cache hit, ms saved. > vp run --last-details @@ -40,11 +51,11 @@ vp run: cache hit, ms saved. Vite+ Task Runner • Execution Summary ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Statistics: 1 tasks • 1 cache hits • 0 cache misses -Performance: 100% cache hit rate, ms saved in total +Statistics: 1 tasks • 0 cache hits • 0 cache misses • 1 cache disabled +Performance: 0% cache hit rate Task Details: ──────────────────────────────────────────────── - [1] npm-install-with-options#install: $ vp install --production --silent ✓ - → Cache hit - output replayed - ms saved + [1] npm-install-with-options#install: $ vp install --prod --silent ✓ + → Cache disabled in task configuration ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/packages/cli/snap-tests/npm-install-with-options/vite.config.ts b/packages/cli/snap-tests/npm-install-with-options/vite.config.ts index f4c86f2d1b..03ba99dcd3 100644 --- a/packages/cli/snap-tests/npm-install-with-options/vite.config.ts +++ b/packages/cli/snap-tests/npm-install-with-options/vite.config.ts @@ -3,7 +3,7 @@ export default { cache: true, tasks: { install: { - command: 'vp install --production --silent', + command: 'vp install --prod --silent', input: [{ auto: true }, '!node_modules/**', '!package-lock.json'], }, }, diff --git a/packages/cli/snap-tests/vite-task-path-env-include-pm/snap.txt b/packages/cli/snap-tests/vite-task-path-env-include-pm/snap.txt index 9128904048..e7e8c1b49c 100644 --- a/packages/cli/snap-tests/vite-task-path-env-include-pm/snap.txt +++ b/packages/cli/snap-tests/vite-task-path-env-include-pm/snap.txt @@ -1,6 +1,4 @@ > vp install --no-frozen-lockfile -➤ YN0050: The --frozen-lockfile option is deprecated; use --immutable and/or --immutable-cache instead - ➤ YN0000: · Yarn ➤ YN0000: ┌ Resolution step ➤ YN0000: └ Completed diff --git a/packages/cli/snap-tests/yarn-install-with-options/snap.txt b/packages/cli/snap-tests/yarn-install-with-options/snap.txt index a53614f379..8282a8ac26 100644 --- a/packages/cli/snap-tests/yarn-install-with-options/snap.txt +++ b/packages/cli/snap-tests/yarn-install-with-options/snap.txt @@ -1,71 +1,40 @@ > vp install --help # print help message - - Usage: yarn install [flags] - - Yarn install is used to install all dependencies for a project. - - Options: - - -v, --version output the version number - --no-default-rc prevent Yarn from automatically detecting yarnrc and npmrc files - --use-yarnrc specifies a yarnrc file that Yarn should use (.yarnrc only, not .npmrc) (default: ) - --verbose output verbose messages on internal operations - --offline trigger an error if any required dependencies are not available in local cache - --prefer-offline use network only if dependencies are not available in local cache - --enable-pnp, --pnp enable the Plug'n'Play installation - --disable-pnp disable the Plug'n'Play installation - --strict-semver - --json format Yarn log messages as lines of JSON (see jsonlines.org) - --ignore-scripts don't run lifecycle scripts - --har save HAR output of network traffic - --ignore-platform ignore platform checks - --ignore-engines ignore engines check - --ignore-optional ignore optional dependencies - --force install and build packages even if they were built before, overwrite lockfile - --skip-integrity-check run install without checking if node_modules is installed - --check-files install will verify file tree of packages for consistency - --no-bin-links don't generate bin links when setting up packages - --flat only allow one version of a package - --prod, --production [prod] - --no-lockfile don't read or generate a lockfile - --pure-lockfile don't generate a lockfile - --frozen-lockfile don't generate a lockfile and fail if an update is needed - --update-checksums update package checksums from current repository - --link-duplicates create hardlinks to the repeated modules in node_modules - --link-folder specify a custom folder to store global links - --global-folder specify a custom folder to store global packages - --modules-folder rather than installing modules into the node_modules folder relative to the cwd, output them here - --preferred-cache-folder specify a custom folder to store the yarn cache if possible - --cache-folder specify a custom folder that must be used to store the yarn cache - --mutex [:specifier] use a mutex to ensure only one yarn instance is executing - --emoji [bool] enable emoji in output (default: true) - -s, --silent skip Yarn console logs, other types of logs (script output) will be printed - --cwd working directory to use (default: ) - --proxy - --https-proxy - --registry override configuration registry - --no-progress disable progress bar - --network-concurrency maximum number of concurrent network requests - --network-timeout TCP timeout for network requests - --non-interactive do not show interactive prompts - --scripts-prepend-node-path [bool] prepend the node executable dir to the PATH in scripts - --no-node-version-check do not warn when using a potentially unsupported Node version - --focus Focus on a single workspace by installing remote copies of its sibling workspaces. - --otp one-time password for two factor authentication - -A, --audit Run vulnerability audit on installed packages - -g, --global DEPRECATED - -S, --save DEPRECATED - save package to your `dependencies` - -D, --save-dev DEPRECATED - save package to your `devDependencies` - -P, --save-peer DEPRECATED - save package to your `peerDependencies` - -O, --save-optional DEPRECATED - save package to your `optionalDependencies` - -E, --save-exact DEPRECATED - -T, --save-tilde DEPRECATED - -h, --help output usage information - Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command. - +Install all dependencies, or add packages if package names are provided + +Usage: vp install [OPTIONS] [PACKAGES]... [-- ...] + +Arguments: + [PACKAGES]... Packages to add (if provided, acts as `vp add`) + [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager + +Options: + -P, --prod Do not install devDependencies + -D, --dev Only install devDependencies (install) / Save to devDependencies (add) + --no-optional Do not install optionalDependencies + --frozen-lockfile Fail if lockfile needs to be updated (CI mode) + --no-frozen-lockfile Allow lockfile updates (opposite of --frozen-lockfile) + --lockfile-only Only update lockfile, don't install + --prefer-offline Use cached packages when available + --offline Only use packages already in cache + -f, --force Force reinstall all dependencies + --ignore-scripts Do not run lifecycle scripts + --no-lockfile Don't read or generate lockfile + --fix-lockfile Fix broken lockfile entries (pnpm and yarn@2+ only) + --shamefully-hoist Create flat `node_modules` (pnpm only) + --resolution-only Re-run resolution for peer dependency analysis (pnpm only) + --silent Suppress output (silent mode) + --filter Filter packages in monorepo (can be used multiple times) + -w, --workspace-root Install in workspace root only + -E, --save-exact Save exact version (only when adding packages) + --save-peer Save to peerDependencies (only when adding packages) + -O, --save-optional Save to optionalDependencies (only when adding packages) + --save-catalog Save the new dependency to the default catalog (only when adding packages) + -g, --global Install globally (requires package names) + --node Node.js version to use for global installation (only with -g) + -h, --help Print help > vp run install -$ vp install --prod +$ vp install --prod ⊘ cache disabled yarn install v info No lockfile found. [1/4] Resolving packages... @@ -75,14 +44,12 @@ info No lockfile found. success Saved lockfile. Done in ms. ---- -vp run: yarn-install-with-options#install not cached because it modified its input. (Run `vp run --last-details` for full details) > ls node_modules tslib > vp run install # install again hit cache -$ vp install --prod +$ vp install --prod ⊘ cache disabled yarn install v [1/4] Resolving packages... success Already up-to-date. diff --git a/rfcs/global-cli-rust-binary.md b/rfcs/global-cli-rust-binary.md index a4706cfffc..9153948d15 100644 --- a/rfcs/global-cli-rust-binary.md +++ b/rfcs/global-cli-rust-binary.md @@ -405,19 +405,8 @@ impl JsExecutor { - `crates/vite_global_cli/Cargo.toml` - `crates/vite_global_cli/src/main.rs` -- `crates/vite_global_cli/src/cli.rs` +- `crates/vite_global_cli/src/cli.rs` # Top-level clap parser; flattens `vite_pm_cli::PackageManagerCommand` for all PM subcommands and intercepts `--global` for managed installs - `crates/vite_global_cli/src/commands/mod.rs` -- `crates/vite_global_cli/src/commands/add.rs` # Add packages (struct-based: AddCommand) -- `crates/vite_global_cli/src/commands/install.rs` # Install dependencies (struct-based: InstallCommand) -- `crates/vite_global_cli/src/commands/remove.rs` # Remove packages (struct-based: RemoveCommand) -- `crates/vite_global_cli/src/commands/update.rs` # Update packages (struct-based: UpdateCommand) -- `crates/vite_global_cli/src/commands/dedupe.rs` # Deduplicate deps (struct-based: DedupeCommand) -- `crates/vite_global_cli/src/commands/outdated.rs` # Check outdated (struct-based: OutdatedCommand) -- `crates/vite_global_cli/src/commands/why.rs` # Explain dependency (struct-based: WhyCommand) -- `crates/vite_global_cli/src/commands/link.rs` # Link packages (struct-based: LinkCommand) -- `crates/vite_global_cli/src/commands/unlink.rs` # Unlink packages (struct-based: UnlinkCommand) -- `crates/vite_global_cli/src/commands/dlx.rs` # Execute package (struct-based: DlxCommand) -- `crates/vite_global_cli/src/commands/pm.rs` # PM subcommands (prune, pack, list, etc.) - `crates/vite_global_cli/src/commands/new.rs` # Project scaffolding - `crates/vite_global_cli/src/commands/migrate.rs` # Migration command - `crates/vite_global_cli/src/commands/delegate.rs` # Local CLI delegation @@ -425,6 +414,8 @@ impl JsExecutor { - `crates/vite_global_cli/src/js_executor.rs` - `crates/vite_global_cli/src/error.rs` +> **Note:** PM command clap definitions and dispatch (`add`, `install`, `remove`, `update`, `dedupe`, `outdated`, `why`, `info`, `link`, `unlink`, `dlx`, `pm `) live in the shared `crates/vite_pm_cli/` crate so they can be reused by both `vite_global_cli` and the local CLI's NAPI binding (`packages/cli/binding/`). The earlier per-command modules under `crates/vite_global_cli/src/commands/` (`add.rs`, `install.rs`, `remove.rs`, …) have been removed in favour of `vite_pm_cli::dispatch`. + **Success Criteria:** - [x] All PM commands work without pre-installed Node.js (uses managed Node.js) diff --git a/rfcs/merge-global-and-local-cli.md b/rfcs/merge-global-and-local-cli.md index 548d928b8b..aa0f01eb90 100644 --- a/rfcs/merge-global-and-local-cli.md +++ b/rfcs/merge-global-and-local-cli.md @@ -122,8 +122,8 @@ The Rust `vp` binary (`crates/vite_global_cli/`) routes commands in two categori │ (Rust) │ │ (Node.js) │ └───────┬────────┘ └───────┬────────┘ │ │ - Handled in oxc_resolver finds - Rust directly local vite-plus + vite_pm_cli:: oxc_resolver finds + dispatch local vite-plus │ │ ▼ ┌─────┴─────┐ ┌────────────────┐ │ found? │ @@ -146,6 +146,12 @@ The Rust `vp` binary (`crates/vite_global_cli/`) routes commands in two categori │ lint, fmt, run │ │ → NAPI │ ├────────────────┤ + │ install, add, │ + │ remove, update │ + │ dlx, pm <…> │ + │ → NAPI │ + │ → vite_pm_cli │ + ├────────────────┤ │ create, migrate │ │ --version │ │ → dist/ │ @@ -153,8 +159,8 @@ The Rust `vp` binary (`crates/vite_global_cli/`) routes commands in two categori └────────────────┘ ``` -- **Category A (Package Manager)**: `install`, `add`, `remove`, `update`, etc. — Handled directly in Rust -- **Category B (JavaScript)**: All other commands (`build`, `test`, `lint`, `create`, `migrate`, `--version`, etc.) — Rust uses `oxc_resolver` to find the project's local `vite-plus/dist/bin.js` and runs it. Falls back to the global installation's `dist/bin.js` if no local installation exists. The unified `bin.ts` entry point then routes to either NAPI bindings (task commands) or rolldown-bundled modules in `dist/global/` (create, migrate, version). +- **Category A (Package Manager)**: `install`, `add`, `remove`, `update`, `dedupe`, `outdated`, `why`, `info`, `link`, `unlink`, `dlx`, `pm ` — clap definitions and dispatch live in the shared `crates/vite_pm_cli/` crate. Both the global CLI and the local CLI binding flatten `vite_pm_cli::PackageManagerCommand` into their top-level argument parser and call `vite_pm_cli::dispatch` to run the underlying package manager (pnpm/npm/yarn/bun). The global CLI additionally intercepts `--global` for vite-plus-managed installs (`commands::env::global_install`) before delegating. +- **Category B (JavaScript)**: All other commands (`build`, `test`, `lint`, `create`, `migrate`, `--version`, etc.) — Rust uses `oxc_resolver` to find the project's local `vite-plus/dist/bin.js` and runs it. Falls back to the global installation's `dist/bin.js` if no local installation exists. The unified `bin.ts` entry point then routes to either NAPI bindings (task commands and PM commands, the latter via `vite_pm_cli::dispatch`) or rolldown-bundled modules in `dist/global/` (create, migrate, version). ### Global scripts_dir Resolution (Rust) @@ -262,6 +268,13 @@ if (command === 'create') { - Upgrade registry (`registry.rs`) queries CLI packages directly instead of looking up optionalDependencies - Reduces download size for `npm install vite-plus` (no longer includes unused `vp` binary) +11. **Brought all PM commands to the local CLI** via a shared `vite_pm_cli` crate: + - Extracted clap definitions and dispatcher for every PM command (`install`, `add`, `remove`, `update`, `dedupe`, `outdated`, `why`, `info`, `link`, `unlink`, `dlx`, `pm `) into `crates/vite_pm_cli/`. Both `vite_global_cli` and the `packages/cli/binding/` NAPI crate flatten `PackageManagerCommand` into their top-level argument parser and call `vite_pm_cli::dispatch`. + - Previously the local CLI binding only knew the `install` shortcut; every other PM command produced clap's "unknown subcommand" error. Now `npx vp add `, `vp remove`, `vp pm publish`, etc. all work identically on global and local. + - The global CLI keeps a thin wrapper for `--global` paths (`commands::env::global_install`) that intercepts before delegating to `vite_pm_cli::dispatch`. The local CLI delegates directly and bypasses the vite-task scheduler since PM operations don't need caching. + - Deleted per-command modules `crates/vite_global_cli/src/commands/{add,remove,install,update,dedupe,outdated,why,link,unlink,dlx,pm}.rs`. + - Mirrored one representative pnpm10 fixture per command into `packages/cli/snap-tests/` to lock in parity. + ## Verification - `cargo test -p vite_global_cli` — Rust unit tests pass