From 870527f60cfe3488628c4a53acb4efccb41a187e Mon Sep 17 00:00:00 2001 From: JongKyung Lee Date: Fri, 21 Aug 2026 23:01:35 +0900 Subject: [PATCH 1/5] refactor(cli): extract global-only commands into vp_global_only_cli Move the env, upgrade, and implode clap definitions out of crates/vp_global_cli/src/cli.rs into a shared crate as GlobalOnlyCommand, and flatten it back into the global Commands enum. The crate also exposes is_global_only(name), derived from the enum through clap, so the names can be recognised elsewhere without a hand-maintained list that can drift from the global surface. The global CLI re-exports the moved types, so commands/env/* is unchanged. No behavior change; global PTY snapshots pass without updates. --- Cargo.lock | 8 + Cargo.toml | 1 + crates/vp_global_cli/Cargo.toml | 1 + crates/vp_global_cli/src/cli.rs | 292 +------------------ crates/vp_global_cli/src/upgrade_check.rs | 6 +- crates/vp_global_only_cli/Cargo.toml | 17 ++ crates/vp_global_only_cli/src/lib.rs | 327 ++++++++++++++++++++++ 7 files changed, 372 insertions(+), 280 deletions(-) create mode 100644 crates/vp_global_only_cli/Cargo.toml create mode 100644 crates/vp_global_only_cli/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 7c1e33d53a..72c54239da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8544,6 +8544,7 @@ dependencies = [ "uuid", "vp_command", "vp_error", + "vp_global_only_cli", "vp_js_runtime", "vp_pm_cli", "vp_setup", @@ -8554,6 +8555,13 @@ dependencies = [ "vt_workspace", ] +[[package]] +name = "vp_global_only_cli" +version = "0.0.0" +dependencies = [ + "clap", +] + [[package]] name = "vp_installer" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index f8a4368d91..21d7483267 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -291,6 +291,7 @@ uuid = "1.17.0" vfs = "0.13.0" vp_command = { path = "crates/vp_command" } vp_error = { path = "crates/vp_error" } +vp_global_only_cli = { path = "crates/vp_global_only_cli" } vp_js_runtime = { path = "crates/vp_js_runtime" } vp_migration = { path = "crates/vp_migration" } vp_pm_cli = { path = "crates/vp_pm_cli" } diff --git a/crates/vp_global_cli/Cargo.toml b/crates/vp_global_cli/Cargo.toml index 6737e82e6f..7e0487e5b9 100644 --- a/crates/vp_global_cli/Cargo.toml +++ b/crates/vp_global_cli/Cargo.toml @@ -36,6 +36,7 @@ indexmap = { workspace = true } indicatif = { workspace = true } indoc = { workspace = true } vp_error = { workspace = true } +vp_global_only_cli = { workspace = true } vp_js_runtime = { workspace = true } vp_pm_cli = { workspace = true } vt_path = { workspace = true } diff --git a/crates/vp_global_cli/src/cli.rs b/crates/vp_global_cli/src/cli.rs index d07f31cf33..50840eb7ae 100644 --- a/crates/vp_global_cli/src/cli.rs +++ b/crates/vp_global_cli/src/cli.rs @@ -10,6 +10,8 @@ use clap_complete::ArgValueCompleter; use dialoguer::{Confirm, theme::ColorfulTheme}; use owo_colors::OwoColorize; use tokio::runtime::Runtime; +use vp_global_only_cli::GlobalOnlyCommand; +pub use vp_global_only_cli::{EnvArgs, EnvSubcommands, PinTarget, SortingMethod}; use vp_pm_cli::{ManagedGlobalCommand, PackageManagerCommand}; use vp_shared::output; use vt_path::AbsolutePathBuf; @@ -217,53 +219,12 @@ pub enum Commands { global: bool, }, - /// Manage Node.js versions - Env(EnvArgs), - // ========================================================================= - // Self-Management + // Category D: Global-only commands (env, upgrade, implode) + // (clap-flattened from the shared `vp_global_only_cli::GlobalOnlyCommand`.) // ========================================================================= - /// Update vp itself to the latest version - #[command(name = "upgrade")] - Upgrade { - /// Target version (e.g., "0.2.0"). Defaults to latest. - version: Option, - - /// npm dist-tag to install (default: "latest", also: "alpha") - #[arg(long, default_value = "latest")] - tag: String, - - /// Check for updates without installing - #[arg(long)] - check: bool, - - /// Revert to the previously active version - #[arg(long)] - rollback: bool, - - /// Force reinstall even if already on the target version - #[arg(long)] - force: bool, - - /// Suppress output - #[arg(long)] - silent: bool, - - /// Custom npm registry URL - #[arg(long)] - registry: Option, - - /// Refresh the cached update status without producing output - #[arg(long, hide = true)] - background_check: bool, - }, - - /// Remove vp and all related data - Implode { - /// Skip confirmation prompt - #[arg(long, short = 'y')] - yes: bool, - }, + #[command(flatten)] + GlobalOnly(GlobalOnlyCommand), } impl Commands { @@ -273,239 +234,12 @@ impl Commands { match self { Self::PackageManager(pm) => pm.is_quiet_or_machine_readable(), Self::Toolchain { json, .. } => *json, - Self::Upgrade { silent, .. } => *silent, - Self::Env(args) => { - args.command.as_ref().is_some_and(|sub| sub.is_quiet_or_machine_readable()) - } - _ => false, - } - } -} - -/// Arguments for the `env` command -#[derive(clap::Args, Debug)] -pub struct EnvArgs { - /// Subcommand (e.g., 'default', 'setup', 'doctor', 'which') - #[command(subcommand)] - pub command: Option, -} - -/// Subcommands for the `env` command -#[derive(clap::Subcommand, Debug)] -pub enum EnvSubcommands { - /// Show current environment information - Current { - /// Output in JSON format - #[arg(long)] - json: bool, - }, - - /// Print shell snippet to set environment for current session - Print, - - /// Set or show the global default Node.js version - #[command(after_long_help = "\ -Examples: - vp env default # Show the current default - vp env default lts # Set the default")] - Default { - /// Version to set as default (e.g., "20.18.0", "lts", "latest"). - /// If omitted, prints the current default. - version: Option, - }, - - /// Enable managed mode - shims always use vite-plus managed Node.js - On, - - /// Enable system-first mode - shims prefer system Node.js, fallback to managed - Off, - - /// Create or update shims in VP_HOME/bin - Setup { - /// Force refresh shims even if they exist - #[arg(long)] - refresh: bool, - /// Only create env files (skip shims and instructions) - #[arg(long)] - env_only: bool, - }, - - /// Run diagnostics and show environment status - Doctor, - - /// Show path to the tool that would be executed - Which { - /// Tool name (node, npm, or npx) - tool: String, - }, - - /// Pin a Node.js version in the current directory - /// (updates .node-version or package.json#devEngines.runtime) - #[command(after_long_help = "\ -Examples: - vp env pin lts # Pin to latest LTS - vp env pin --unpin # Remove the pin - vp env pin \"^20.0.0\" --force # Overwrite existing pin - vp env pin 24 --target node-version # Force the .node-version file - -The write target follows the compatibility-first rule: an existing .node-version -keeps being updated; otherwise the pin is written to package.json#devEngines.runtime; -.node-version is only created when the directory has no package.json.")] - Pin { - /// Version to pin (e.g., "20.18.0", "lts", "latest", "^20.0.0"). - /// If omitted, prints the currently pinned version. - version: Option, - - /// Remove the pin from the current directory - #[arg(long)] - unpin: bool, - - /// Skip pre-downloading the pinned version - #[arg(long)] - no_install: bool, - - /// Overwrite an existing pin without confirmation - #[arg(long)] - force: bool, - - /// Explicitly choose the write target (overrides the default selection) - #[arg(long, value_enum)] - target: Option, - }, - - /// Remove the Node.js pin from current directory (alias for `pin --unpin`) - Unpin { - /// Explicitly choose which pin source to remove - #[arg(long, value_enum)] - target: Option, - }, - - /// List locally installed Node.js versions - #[command(visible_alias = "ls")] - List { - /// Output as JSON - #[arg(long)] - json: bool, - }, - - /// List available Node.js versions from the registry - #[command(name = "list-remote", visible_alias = "ls-remote")] - ListRemote { - /// Filter versions by pattern (e.g., "20" for 20.x versions) - pattern: Option, - - /// Show only LTS versions - #[arg(long)] - lts: bool, - - /// Show all versions (not just recent) - #[arg(long)] - all: bool, - - /// Output as JSON - #[arg(long)] - json: bool, - - /// Version sorting order - #[arg(long, value_enum, default_value_t = SortingMethod::Asc)] - sort: SortingMethod, - }, - - /// Execute a command with a specific Node.js version - #[command( - visible_alias = "run", - after_long_help = "\ -Examples: - vp env exec --node lts npm install # Pin version for this invocation - vp env exec node -v # Shim mode: version auto-resolved" - )] - Exec { - /// Node.js version to use (e.g., "20.18.0", "lts", "^20.0.0"). - /// If omitted and command is node/npm/npx or a global package binary, - /// version is resolved automatically (same as shim behavior). - #[arg(long)] - node: Option, - - /// npm version to use (optional, defaults to bundled) - #[arg(long)] - npm: Option, - - /// Command and arguments to run - #[arg(trailing_var_arg = true, allow_hyphen_values = true)] - command: Vec, - }, - - /// Uninstall a Node.js version - #[command(visible_alias = "uni")] - Uninstall { - /// Version to uninstall (e.g., "20.18.0") - #[arg(required = true)] - version: String, - }, - - /// Remove unused managed runtimes and package manager caches - Clean, - - /// Install a Node.js version - #[command(visible_alias = "i")] - Install { - /// Version to install (e.g., "20", "20.18.0", "lts", "latest") - /// If not provided, installs the version from .node-version, package.json, or .nvmrc - version: Option, - }, - - /// Use a specific Node.js version for this shell session - #[command(after_long_help = "\ -Examples: - vp env use lts # Override session with latest LTS - vp env use --unset # Clear the session override")] - Use { - /// Version to use (e.g., "20", "20.18.0", "lts", "latest"). - /// If omitted, reads from .node-version, package.json, or .nvmrc. - version: Option, - - /// Remove session override (revert to file-based resolution) - #[arg(long)] - unset: bool, - - /// Skip auto-installation if version not present - #[arg(long)] - no_install: bool, - - /// Suppress output if version is already active - #[arg(long)] - silent_if_unchanged: bool, - }, -} - -impl EnvSubcommands { - fn is_quiet_or_machine_readable(&self) -> bool { - match self { - Self::Current { json } | Self::List { json } | Self::ListRemote { json, .. } => *json, + Self::GlobalOnly(cmd) => cmd.is_quiet_or_machine_readable(), _ => false, } } } -/// Write target for `vp env pin` / `vp env unpin` (see rfcs/dev-engines.md) -#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)] -pub enum PinTarget { - /// Pin via the .node-version file - NodeVersion, - /// Pin via package.json#devEngines.runtime - DevEngines, -} - -/// Version sorting order for list-remote command -#[derive(clap::ValueEnum, Clone, Debug, Default)] -pub enum SortingMethod { - /// Sort versions in ascending order (earliest to latest) - #[default] - Asc, - /// Sort versions in descending order (latest to earliest) - Desc, -} - fn has_flag_before_terminator(args: &[String], flag: &str) -> bool { for arg in args { if arg == "--" { @@ -1093,10 +827,12 @@ pub async fn run_command_with_options( commands::toolchain::execute(cwd, tools, json, global, raw_subcommand).await } - Commands::Env(args) => commands::env::execute(cwd, args).await, + // Category D: Global-only commands + Commands::GlobalOnly(GlobalOnlyCommand::Env(args)) => { + commands::env::execute(cwd, args).await + } - // Self-Management - Commands::Upgrade { + Commands::GlobalOnly(GlobalOnlyCommand::Upgrade { version, tag, check, @@ -1105,7 +841,7 @@ pub async fn run_command_with_options( silent, registry, background_check, - } => { + }) => { commands::upgrade::execute(commands::upgrade::UpgradeOptions { version, tag, @@ -1118,7 +854,7 @@ pub async fn run_command_with_options( }) .await } - Commands::Implode { yes } => commands::implode::execute(yes), + Commands::GlobalOnly(GlobalOnlyCommand::Implode { yes }) => commands::implode::execute(yes), } } diff --git a/crates/vp_global_cli/src/upgrade_check.rs b/crates/vp_global_cli/src/upgrade_check.rs index 61a09516ed..79f7c869f1 100644 --- a/crates/vp_global_cli/src/upgrade_check.rs +++ b/crates/vp_global_cli/src/upgrade_check.rs @@ -334,8 +334,10 @@ pub fn should_run_for_command(args: &crate::cli::Args) -> bool { match &args.command { Some( - crate::cli::Commands::Upgrade { .. } - | crate::cli::Commands::Implode { .. } + crate::cli::Commands::GlobalOnly( + vp_global_only_cli::GlobalOnlyCommand::Upgrade { .. } + | vp_global_only_cli::GlobalOnlyCommand::Implode { .. }, + ) | crate::cli::Commands::Lint { .. } | crate::cli::Commands::Fmt { .. }, ) => false, diff --git a/crates/vp_global_only_cli/Cargo.toml b/crates/vp_global_only_cli/Cargo.toml new file mode 100644 index 0000000000..aa2bce275a --- /dev/null +++ b/crates/vp_global_only_cli/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "vp_global_only_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"] } + +[lib] +doctest = false + +[lints] +workspace = true diff --git a/crates/vp_global_only_cli/src/lib.rs b/crates/vp_global_only_cli/src/lib.rs new file mode 100644 index 0000000000..1d2b66bc9e --- /dev/null +++ b/crates/vp_global_only_cli/src/lib.rs @@ -0,0 +1,327 @@ +//! Commands that exist only in the global `vp` binary. +//! +//! The global CLI flattens [`GlobalOnlyCommand`] into its top-level command +//! enum. [`is_global_only`] derives the command names from the same enum so +//! other crates can recognise them without keeping a separate list. + +#![allow(clippy::allow_attributes, clippy::disallowed_types)] + +use clap::{Subcommand, builder::Command}; + +/// Commands that only the global `vp` binary can run. +#[derive(Subcommand, Debug)] +pub enum GlobalOnlyCommand { + /// Manage Node.js versions + Env(EnvArgs), + + // ========================================================================= + // Self-Management + // ========================================================================= + /// Update vp itself to the latest version + #[command(name = "upgrade")] + Upgrade { + /// Target version (e.g., "0.2.0"). Defaults to latest. + version: Option, + + /// npm dist-tag to install (default: "latest", also: "alpha") + #[arg(long, default_value = "latest")] + tag: String, + + /// Check for updates without installing + #[arg(long)] + check: bool, + + /// Revert to the previously active version + #[arg(long)] + rollback: bool, + + /// Force reinstall even if already on the target version + #[arg(long)] + force: bool, + + /// Suppress output + #[arg(long)] + silent: bool, + + /// Custom npm registry URL + #[arg(long)] + registry: Option, + + /// Refresh the cached update status without producing output + #[arg(long, hide = true)] + background_check: bool, + }, + + /// Remove vp and all related data + Implode { + /// Skip confirmation prompt + #[arg(long, short = 'y')] + yes: bool, + }, +} + +impl GlobalOnlyCommand { + /// 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::Upgrade { silent, .. } => *silent, + Self::Env(args) => { + args.command.as_ref().is_some_and(|sub| sub.is_quiet_or_machine_readable()) + } + Self::Implode { .. } => false, + } + } +} + +/// Whether `name` (a subcommand name or alias) is only available in the +/// global `vp` binary. Derived from [`GlobalOnlyCommand`] so there is no +/// separate list to keep in sync. +pub fn is_global_only(name: &str) -> bool { + GlobalOnlyCommand::augment_subcommands(Command::new("vp")) + .get_subcommands() + .any(|cmd| cmd.get_name() == name || cmd.get_all_aliases().any(|alias| alias == name)) +} + +/// Arguments for the `env` command +#[derive(clap::Args, Debug)] +pub struct EnvArgs { + /// Subcommand (e.g., 'default', 'setup', 'doctor', 'which') + #[command(subcommand)] + pub command: Option, +} + +/// Subcommands for the `env` command +#[derive(clap::Subcommand, Debug)] +pub enum EnvSubcommands { + /// Show current environment information + Current { + /// Output in JSON format + #[arg(long)] + json: bool, + }, + + /// Print shell snippet to set environment for current session + Print, + + /// Set or show the global default Node.js version + #[command(after_long_help = "\ +Examples: + vp env default # Show the current default + vp env default lts # Set the default")] + Default { + /// Version to set as default (e.g., "20.18.0", "lts", "latest"). + /// If omitted, prints the current default. + version: Option, + }, + + /// Enable managed mode - shims always use vite-plus managed Node.js + On, + + /// Enable system-first mode - shims prefer system Node.js, fallback to managed + Off, + + /// Create or update shims in VP_HOME/bin + Setup { + /// Force refresh shims even if they exist + #[arg(long)] + refresh: bool, + /// Only create env files (skip shims and instructions) + #[arg(long)] + env_only: bool, + }, + + /// Run diagnostics and show environment status + Doctor, + + /// Show path to the tool that would be executed + Which { + /// Tool name (node, npm, or npx) + tool: String, + }, + + /// Pin a Node.js version in the current directory + /// (updates .node-version or package.json#devEngines.runtime) + #[command(after_long_help = "\ +Examples: + vp env pin lts # Pin to latest LTS + vp env pin --unpin # Remove the pin + vp env pin \"^20.0.0\" --force # Overwrite existing pin + vp env pin 24 --target node-version # Force the .node-version file + +The write target follows the compatibility-first rule: an existing .node-version +keeps being updated; otherwise the pin is written to package.json#devEngines.runtime; +.node-version is only created when the directory has no package.json.")] + Pin { + /// Version to pin (e.g., "20.18.0", "lts", "latest", "^20.0.0"). + /// If omitted, prints the currently pinned version. + version: Option, + + /// Remove the pin from the current directory + #[arg(long)] + unpin: bool, + + /// Skip pre-downloading the pinned version + #[arg(long)] + no_install: bool, + + /// Overwrite an existing pin without confirmation + #[arg(long)] + force: bool, + + /// Explicitly choose the write target (overrides the default selection) + #[arg(long, value_enum)] + target: Option, + }, + + /// Remove the Node.js pin from current directory (alias for `pin --unpin`) + Unpin { + /// Explicitly choose which pin source to remove + #[arg(long, value_enum)] + target: Option, + }, + + /// List locally installed Node.js versions + #[command(visible_alias = "ls")] + List { + /// Output as JSON + #[arg(long)] + json: bool, + }, + + /// List available Node.js versions from the registry + #[command(name = "list-remote", visible_alias = "ls-remote")] + ListRemote { + /// Filter versions by pattern (e.g., "20" for 20.x versions) + pattern: Option, + + /// Show only LTS versions + #[arg(long)] + lts: bool, + + /// Show all versions (not just recent) + #[arg(long)] + all: bool, + + /// Output as JSON + #[arg(long)] + json: bool, + + /// Version sorting order + #[arg(long, value_enum, default_value_t = SortingMethod::Asc)] + sort: SortingMethod, + }, + + /// Execute a command with a specific Node.js version + #[command( + visible_alias = "run", + after_long_help = "\ +Examples: + vp env exec --node lts npm install # Pin version for this invocation + vp env exec node -v # Shim mode: version auto-resolved" + )] + Exec { + /// Node.js version to use (e.g., "20.18.0", "lts", "^20.0.0"). + /// If omitted and command is node/npm/npx or a global package binary, + /// version is resolved automatically (same as shim behavior). + #[arg(long)] + node: Option, + + /// npm version to use (optional, defaults to bundled) + #[arg(long)] + npm: Option, + + /// Command and arguments to run + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + command: Vec, + }, + + /// Uninstall a Node.js version + #[command(visible_alias = "uni")] + Uninstall { + /// Version to uninstall (e.g., "20.18.0") + #[arg(required = true)] + version: String, + }, + + /// Remove unused managed runtimes and package manager caches + Clean, + + /// Install a Node.js version + #[command(visible_alias = "i")] + Install { + /// Version to install (e.g., "20", "20.18.0", "lts", "latest") + /// If not provided, installs the version from .node-version, package.json, or .nvmrc + version: Option, + }, + + /// Use a specific Node.js version for this shell session + #[command(after_long_help = "\ +Examples: + vp env use lts # Override session with latest LTS + vp env use --unset # Clear the session override")] + Use { + /// Version to use (e.g., "20", "20.18.0", "lts", "latest"). + /// If omitted, reads from .node-version, package.json, or .nvmrc. + version: Option, + + /// Remove session override (revert to file-based resolution) + #[arg(long)] + unset: bool, + + /// Skip auto-installation if version not present + #[arg(long)] + no_install: bool, + + /// Suppress output if version is already active + #[arg(long)] + silent_if_unchanged: bool, + }, +} + +impl EnvSubcommands { + pub fn is_quiet_or_machine_readable(&self) -> bool { + match self { + Self::Current { json } | Self::List { json } | Self::ListRemote { json, .. } => *json, + _ => false, + } + } +} + +/// Write target for `vp env pin` / `vp env unpin` (see rfcs/dev-engines.md) +#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)] +pub enum PinTarget { + /// Pin via the .node-version file + NodeVersion, + /// Pin via package.json#devEngines.runtime + DevEngines, +} + +/// Version sorting order for list-remote command +#[derive(clap::ValueEnum, Clone, Debug, Default)] +pub enum SortingMethod { + /// Sort versions in ascending order (earliest to latest) + #[default] + Asc, + /// Sort versions in descending order (latest to earliest) + Desc, +} + +#[cfg(test)] +mod tests { + use super::is_global_only; + + #[test] + fn recognises_global_only_command_names() { + for name in ["env", "upgrade", "implode"] { + assert!(is_global_only(name), "{name} should be global-only"); + } + } + + #[test] + fn rejects_other_command_names() { + for name in ["create", "dev", "install", "run", ""] { + assert!(!is_global_only(name), "{name} should not be global-only"); + } + } +} From c9177086cc3ce387e7fbc6eaeac6b1eb1b386582 Mon Sep 17 00:00:00 2001 From: JongKyung Lee Date: Fri, 21 Aug 2026 23:17:24 +0900 Subject: [PATCH 2/5] docs(rfcs): record the vp_global_only_cli crate Document the shared global-only command crate next to the existing vp_pm_cli notes in merge-global-and-local-cli.md, align the command category letters with cli.rs, and update the file map in global-cli-rust-binary.md. --- rfcs/global-cli-rust-binary.md | 2 +- rfcs/merge-global-and-local-cli.md | 107 ++++++++++++++++------------- 2 files changed, 60 insertions(+), 49 deletions(-) diff --git a/rfcs/global-cli-rust-binary.md b/rfcs/global-cli-rust-binary.md index 66d6029f0d..fab3e0e263 100644 --- a/rfcs/global-cli-rust-binary.md +++ b/rfcs/global-cli-rust-binary.md @@ -405,7 +405,7 @@ impl JsExecutor { - `crates/vp_global_cli/Cargo.toml` - `crates/vp_global_cli/src/main.rs` -- `crates/vp_global_cli/src/cli.rs` # Top-level clap parser; flattens `vp_pm_cli::PackageManagerCommand` for all PM subcommands and intercepts `--global` for managed installs +- `crates/vp_global_cli/src/cli.rs` # Top-level clap parser; flattens `vp_pm_cli::PackageManagerCommand` for all PM subcommands and `vp_global_only_cli::GlobalOnlyCommand` for `env`/`upgrade`/`implode`, and intercepts `--global` for managed installs - `crates/vp_global_cli/src/commands/mod.rs` - `crates/vp_global_cli/src/commands/new.rs` # Project scaffolding - `crates/vp_global_cli/src/commands/migrate.rs` # Migration command diff --git a/rfcs/merge-global-and-local-cli.md b/rfcs/merge-global-and-local-cli.md index 6218cd6299..4165a721eb 100644 --- a/rfcs/merge-global-and-local-cli.md +++ b/rfcs/merge-global-and-local-cli.md @@ -108,59 +108,66 @@ This decouples the `vp` binary from vite-plus's internal file layout. ### Command Routing -The Rust `vp` binary (`crates/vp_global_cli/`) routes commands in two categories: +The Rust `vp` binary (`crates/vp_global_cli/`) routes commands in four categories: ``` - vp - │ - ┌─────────────┴──────────────┐ - │ │ - ▼ ▼ - ┌────────────────┐ ┌────────────────┐ - │ Category A │ │ Category B │ - │ Pkg Mgr │ │ JavaScript │ - │ (Rust) │ │ (Node.js) │ - └───────┬────────┘ └───────┬────────┘ - │ │ - vp_pm_cli:: oxc_resolver finds - dispatch local vite-plus - │ │ - ▼ ┌─────┴─────┐ - ┌────────────────┐ │ found? │ - │ install │ └─────┬─────┘ - │ add │ yes ╱ ╲ no - │ remove │ ╱ ╲ - │ update │ ▼ ▼ - │ ... │ ┌────────┐ ┌────────┐ - └────────────────┘ │ local │ │ global │ - │ bin.js │ │ bin.js │ - └───┬────┘ └───┬────┘ - └─────┬────┘ - │ - ▼ - ┌────────────────┐ - │ bin.ts │ - │ routes to: │ - ├────────────────┤ - │ build, test, │ - │ lint, fmt, run │ - │ → NAPI │ - ├────────────────┤ - │ install, add, │ - │ remove, update │ - │ dlx, pm <…> │ - │ → NAPI │ - │ → vp_pm_cli │ - ├────────────────┤ - │ create, migrate │ - │ --version │ - │ → dist/ │ - │ global/*.js │ - └────────────────┘ + vp + │ + ┌─────────────────┬───────────┴───────────┬─────────────────┐ + │ │ │ │ + ▼ ▼ ▼ ▼ + ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ + │ Category A │ │ Category B │ │ Category C │ │ Category D │ + │ Pkg Mgr │ │ JavaScript │ │ Local CLI │ │ Global-only │ + │ (Rust) │ │ (Node.js) │ │ Delegation │ │ (Rust) │ + └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ + │ │ │ │ + vp_pm_cli:: └──────────┬───────────┘ vp_global_only_cli:: + dispatch │ GlobalOnlyCommand + │ oxc_resolver finds │ + ▼ local vite-plus ▼ + ┌──────────────┐ │ ┌──────────────┐ + │ install │ ┌─────┴─────┐ │ env │ + │ add │ │ found? │ │ upgrade │ + │ remove │ └─────┬─────┘ │ implode │ + │ update │ yes ╱ ╲ no └──────────────┘ + │ ... │ ╱ ╲ commands::{env, + └──────────────┘ ▼ ▼ upgrade,implode} + ┌────────┐ ┌────────┐ + │ local │ │ global │ + │ bin.js │ │ bin.js │ + └───┬────┘ └───┬────┘ + └─────┬────┘ + │ + ▼ + ┌─────────────────┐ + │ bin.ts │ + │ routes to: │ + ├─────────────────┤ + │ B: create, │ + │ migrate, │ + │ config, │ + │ hooks, │ + │ staged, │ + │ --version │ + │ → dist/*.js │ + ├─────────────────┤ + │ C: build, test, │ + │ lint, fmt, │ + │ run, ... │ + │ → NAPI │ + ├─────────────────┤ + │ install, add, │ + │ remove, ... │ + │ → NAPI │ + │ → vp_pm_cli │ + └─────────────────┘ ``` - **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/vp_pm_cli/` crate. Both the global CLI and the local CLI binding flatten `vp_pm_cli::PackageManagerCommand` into their top-level argument parser and call `vp_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 `vp_pm_cli::dispatch`) or rolldown-bundled modules in `dist/global/` (create, migrate, version). +- **Category B (JavaScript)**: `create`, `migrate`, `config`, `hooks`, `staged`, `--version` — 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 `vp_pm_cli::dispatch`) or rolldown-bundled modules in `dist/global/` (create, migrate, version). +- **Category C (Local CLI Delegation)**: `dev`, `build`, `test`, `lint`, `fmt`, `check`, `pack`, `run`, `exec`, `preview`, `cache` — forwarded to the project-local `vite-plus` CLI through `commands::delegate`, which resolves it the same way as Category B. `lint --init` and `fmt --init`/`--migrate` are forced to the global installation. +- **Category D (Global-only)**: `env`, `upgrade`, `implode` — clap definitions live in the shared `crates/vp_global_only_cli/` crate as `GlobalOnlyCommand`. The global CLI flattens it into its top-level argument parser and keeps the implementations in `crates/vp_global_cli/src/commands/`. The crate also exposes `is_global_only(name)`, derived from the same enum via clap, so other crates can recognise these names without a separate list. ### Global scripts_dir Resolution (Rust) @@ -275,6 +282,10 @@ if (command === 'create') { - Deleted per-command modules `crates/vp_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. +12. **Extracted global-only commands into a shared `vp_global_only_cli` crate**: + - Moved the `env`, `upgrade`, and `implode` clap definitions (plus `EnvArgs`, `EnvSubcommands`, `PinTarget`, `SortingMethod`) out of `crates/vp_global_cli/src/cli.rs` into `crates/vp_global_only_cli/` as `GlobalOnlyCommand`. The global CLI flattens it into `Commands` and re-exports the types so `commands/env/*` keep their import paths. + - Exposed `is_global_only(name)`, computed from the enum through clap's `augment_subcommands`, so command names can be recognised elsewhere without maintaining a list that could drift from the global surface. + ## Verification - `cargo test -p vp_global_cli` — Rust unit tests pass From 599856ec39992fdd1348956764df4e3ce885de16 Mon Sep 17 00:00:00 2001 From: JongKyung Lee Date: Fri, 21 Aug 2026 23:19:14 +0900 Subject: [PATCH 3/5] docs(rfcs): restore the original routing diagram The redrawn four-category diagram is out of scope for this PR; keep the original two-path diagram and leave the category text as the source of truth. --- rfcs/merge-global-and-local-cli.md | 97 ++++++++++++++---------------- 1 file changed, 46 insertions(+), 51 deletions(-) diff --git a/rfcs/merge-global-and-local-cli.md b/rfcs/merge-global-and-local-cli.md index 4165a721eb..36cf21e2d0 100644 --- a/rfcs/merge-global-and-local-cli.md +++ b/rfcs/merge-global-and-local-cli.md @@ -111,57 +111,52 @@ This decouples the `vp` binary from vite-plus's internal file layout. The Rust `vp` binary (`crates/vp_global_cli/`) routes commands in four categories: ``` - vp - │ - ┌─────────────────┬───────────┴───────────┬─────────────────┐ - │ │ │ │ - ▼ ▼ ▼ ▼ - ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ - │ Category A │ │ Category B │ │ Category C │ │ Category D │ - │ Pkg Mgr │ │ JavaScript │ │ Local CLI │ │ Global-only │ - │ (Rust) │ │ (Node.js) │ │ Delegation │ │ (Rust) │ - └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ - │ │ │ │ - vp_pm_cli:: └──────────┬───────────┘ vp_global_only_cli:: - dispatch │ GlobalOnlyCommand - │ oxc_resolver finds │ - ▼ local vite-plus ▼ - ┌──────────────┐ │ ┌──────────────┐ - │ install │ ┌─────┴─────┐ │ env │ - │ add │ │ found? │ │ upgrade │ - │ remove │ └─────┬─────┘ │ implode │ - │ update │ yes ╱ ╲ no └──────────────┘ - │ ... │ ╱ ╲ commands::{env, - └──────────────┘ ▼ ▼ upgrade,implode} - ┌────────┐ ┌────────┐ - │ local │ │ global │ - │ bin.js │ │ bin.js │ - └───┬────┘ └───┬────┘ - └─────┬────┘ - │ - ▼ - ┌─────────────────┐ - │ bin.ts │ - │ routes to: │ - ├─────────────────┤ - │ B: create, │ - │ migrate, │ - │ config, │ - │ hooks, │ - │ staged, │ - │ --version │ - │ → dist/*.js │ - ├─────────────────┤ - │ C: build, test, │ - │ lint, fmt, │ - │ run, ... │ - │ → NAPI │ - ├─────────────────┤ - │ install, add, │ - │ remove, ... │ - │ → NAPI │ - │ → vp_pm_cli │ - └─────────────────┘ + vp + │ + ┌─────────────┴──────────────┐ + │ │ + ▼ ▼ + ┌────────────────┐ ┌────────────────┐ + │ Category A │ │ Category B │ + │ Pkg Mgr │ │ JavaScript │ + │ (Rust) │ │ (Node.js) │ + └───────┬────────┘ └───────┬────────┘ + │ │ + vp_pm_cli:: oxc_resolver finds + dispatch local vite-plus + │ │ + ▼ ┌─────┴─────┐ + ┌────────────────┐ │ found? │ + │ install │ └─────┬─────┘ + │ add │ yes ╱ ╲ no + │ remove │ ╱ ╲ + │ update │ ▼ ▼ + │ ... │ ┌────────┐ ┌────────┐ + └────────────────┘ │ local │ │ global │ + │ bin.js │ │ bin.js │ + └───┬────┘ └───┬────┘ + └─────┬────┘ + │ + ▼ + ┌────────────────┐ + │ bin.ts │ + │ routes to: │ + ├────────────────┤ + │ build, test, │ + │ lint, fmt, run │ + │ → NAPI │ + ├────────────────┤ + │ install, add, │ + │ remove, update │ + │ dlx, pm <…> │ + │ → NAPI │ + │ → vp_pm_cli │ + ├────────────────┤ + │ create, migrate │ + │ --version │ + │ → dist/ │ + │ global/*.js │ + └────────────────┘ ``` - **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/vp_pm_cli/` crate. Both the global CLI and the local CLI binding flatten `vp_pm_cli::PackageManagerCommand` into their top-level argument parser and call `vp_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. From 8ccd991d1ef92c13028753b134894aaff2c4889b Mon Sep 17 00:00:00 2001 From: JongKyung Lee Date: Fri, 21 Aug 2026 23:19:14 +0900 Subject: [PATCH 4/5] docs(agents): add vp_global_only_cli to the repo map --- AGENTS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 701a619031..4f230e2266 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,6 +34,7 @@ vite-plus/ ├── crates/vp_command/ # Shared command execution helpers ├── crates/vp_error/ # Shared error types ├── crates/vp_global_cli/ # Standalone global vp binary and top-level command routing +├── crates/vp_global_only_cli/ # clap definitions for global-only commands (env, upgrade, implode) ├── crates/vp_installer/ # Installer binary support ├── crates/vp_js_runtime/ # Managed Node.js runtime support ├── crates/vp_migration/ # Rust migration helpers @@ -59,7 +60,7 @@ install is present. Call sites must not construct category paths or read - **JS-backed CLI behavior**: start at `packages/cli/src/bin.ts` and nearby `packages/cli/src/**` files. - **Local CLI / NAPI-backed behavior**: start at `packages/cli/binding/src/lib.rs` and `packages/cli/binding/src/cli/mod.rs`. -- **Global `vp` routing, aliases, and runtime bootstrap**: start at `crates/vp_global_cli/src/main.rs` and `crates/vp_global_cli/src/cli.rs`. +- **Global `vp` routing, aliases, and runtime bootstrap**: start at `crates/vp_global_cli/src/main.rs` and `crates/vp_global_cli/src/cli.rs`; the `env`, `upgrade`, and `implode` argument definitions live in `crates/vp_global_only_cli/`. - **Package-manager behavior**: start at `crates/vp_pm_cli/`. - **Managed Node runtime / shims**: start at `crates/vp_js_runtime/`. - **Static `vite.config.ts` extraction**: start at `crates/vp_static_config/README.md` and `packages/cli/src/resolve-vite-config.ts`. From b17aa69ead963f1ad71f207e41cfeea5cfe14289 Mon Sep 17 00:00:00 2001 From: JongKyung Lee Date: Fri, 21 Aug 2026 23:22:12 +0900 Subject: [PATCH 5/5] docs(rfcs): draw the command routing diagram with mermaid Replace the two-path ASCII diagram with a mermaid flowchart that shows all four routing categories, matching the list below it. --- rfcs/merge-global-and-local-cli.md | 70 ++++++++++-------------------- 1 file changed, 23 insertions(+), 47 deletions(-) diff --git a/rfcs/merge-global-and-local-cli.md b/rfcs/merge-global-and-local-cli.md index 36cf21e2d0..5cc291291e 100644 --- a/rfcs/merge-global-and-local-cli.md +++ b/rfcs/merge-global-and-local-cli.md @@ -110,53 +110,29 @@ This decouples the `vp` binary from vite-plus's internal file layout. The Rust `vp` binary (`crates/vp_global_cli/`) routes commands in four categories: -``` - vp - │ - ┌─────────────┴──────────────┐ - │ │ - ▼ ▼ - ┌────────────────┐ ┌────────────────┐ - │ Category A │ │ Category B │ - │ Pkg Mgr │ │ JavaScript │ - │ (Rust) │ │ (Node.js) │ - └───────┬────────┘ └───────┬────────┘ - │ │ - vp_pm_cli:: oxc_resolver finds - dispatch local vite-plus - │ │ - ▼ ┌─────┴─────┐ - ┌────────────────┐ │ found? │ - │ install │ └─────┬─────┘ - │ add │ yes ╱ ╲ no - │ remove │ ╱ ╲ - │ update │ ▼ ▼ - │ ... │ ┌────────┐ ┌────────┐ - └────────────────┘ │ local │ │ global │ - │ bin.js │ │ bin.js │ - └───┬────┘ └───┬────┘ - └─────┬────┘ - │ - ▼ - ┌────────────────┐ - │ bin.ts │ - │ routes to: │ - ├────────────────┤ - │ build, test, │ - │ lint, fmt, run │ - │ → NAPI │ - ├────────────────┤ - │ install, add, │ - │ remove, update │ - │ dlx, pm <…> │ - │ → NAPI │ - │ → vp_pm_cli │ - ├────────────────┤ - │ create, migrate │ - │ --version │ - │ → dist/ │ - │ global/*.js │ - └────────────────┘ +```mermaid +flowchart TD + vp["vp <command>"] + + vp --> A["Category A
Package Manager (Rust)"] + vp --> B["Category B
JavaScript (Node.js)"] + vp --> C["Category C
Local CLI Delegation"] + vp --> D["Category D
Global-only (Rust)"] + + A --> pm["vp_pm_cli::dispatch
install, add, remove, update, ..."] + + B --> resolve + C --> resolve["oxc_resolver finds local vite-plus"] + resolve -->|found| localBin["local dist/bin.js"] + resolve -->|not found| globalBin["global dist/bin.js"] + localBin --> binTs + globalBin --> binTs["bin.ts"] + binTs --> js["B: create, migrate, config, hooks, staged, --version
→ dist/*.js"] + binTs --> napi["C: build, test, lint, fmt, run, ...
→ NAPI"] + binTs --> napiPm["install, add, remove, ...
→ NAPI → vp_pm_cli"] + + D --> globalOnly["vp_global_only_cli::GlobalOnlyCommand
env, upgrade, implode"] + globalOnly --> impl["commands::{env, upgrade, implode}"] ``` - **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/vp_pm_cli/` crate. Both the global CLI and the local CLI binding flatten `vp_pm_cli::PackageManagerCommand` into their top-level argument parser and call `vp_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.