From 25739b4be0a202f506e5cbd1d43124bb2af5867c Mon Sep 17 00:00:00 2001 From: Liang Date: Fri, 23 Oct 2026 22:10:58 +0800 Subject: [PATCH 1/2] fix(env): report doctor failures as errors --- .../snapshots.toml | 10 +++ .../command_env_doctor_broken_config.md | 44 +++++++++++ .../vp_global_cli/src/commands/env/doctor.rs | 76 ++++++++++++------- 3 files changed, 104 insertions(+), 26 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_doctor_broken_config/snapshots.toml create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_doctor_broken_config/snapshots/command_env_doctor_broken_config.md diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_doctor_broken_config/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_doctor_broken_config/snapshots.toml new file mode 100644 index 0000000000..cc9543528d --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_doctor_broken_config/snapshots.toml @@ -0,0 +1,10 @@ +[[case]] +name = "command_env_doctor_broken_config" +vp = "global" +skip-platforms = ["windows"] +comment = "An unreadable config breaks the shims, so doctor reports it as an error and exits with 1." +steps = [ + { argv = ["vpt", "write-file", "$HOME/.profile", ". \"$HOME/.vite-plus/env\""], snapshot = false, comment = "source the env file so the output has no platform-specific IDE guidance" }, + { argv = ["vpt", "write-file", "$VP_HOME/config.json", "{ not json"], snapshot = false }, + ["vp", "env", "doctor"], +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_doctor_broken_config/snapshots/command_env_doctor_broken_config.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_doctor_broken_config/snapshots/command_env_doctor_broken_config.md new file mode 100644 index 0000000000..873d097c83 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_doctor_broken_config/snapshots/command_env_doctor_broken_config.md @@ -0,0 +1,44 @@ +# command_env_doctor_broken_config + +An unreadable config breaks the shims, so doctor reports it as an error and exits with 1. + +## `vpt write-file $HOME/.profile '. "$HOME/.vite-plus/env"'` + +source the env file so the output has no platform-specific IDE guidance + + +## `vpt write-file $VP_HOME/config.json '{ not json'` + + +## `vp env doctor` + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +Installation + ✓ Bin dir ~/.vite-plus/bin + ✓ Data dir ~/.vite-plus + ✓ Cache dir ~/.vite-plus/cache (not created yet) + ✓ Config dir ~/.vite-plus + ✓ State dir ~/.vite-plus + +Configuration + ✗ Config failed to read: JSON error: key must be a string at line 1 column 3 + note: Fix or remove ~/.vite-plus/config.json. + ✓ IDE integration env sourced in ~/.profile + +PATH + ✓ vp ~/.vite-plus/bin/vp + ✓ Shim dir ~/.vite-plus/bin + +Node.js Resolution + Directory + ✗ Resolution failed: JSON error: key must be a string at line 1 column 3 + +Package Manager Resolution + Package manager not selected + +✗ Some issues found. Run the suggested commands to fix them. +``` diff --git a/crates/vp_global_cli/src/commands/env/doctor.rs b/crates/vp_global_cli/src/commands/env/doctor.rs index e19d10f40a..6676e88c86 100644 --- a/crates/vp_global_cli/src/commands/env/doctor.rs +++ b/crates/vp_global_cli/src/commands/env/doctor.rs @@ -106,7 +106,13 @@ pub async fn execute(cwd: AbsolutePathBuf, scope: Option) -> Result result, + None => { + has_errors = true; + (config::Config::default(), None) + } + }; // Check env sourcing: IDE-relevant profiles first, then all shell profiles let env_status = cfg!(not(windows)).then(check_env_sourcing); @@ -125,7 +131,11 @@ pub async fn execute(cwd: AbsolutePathBuf, scope: Option) -> Result bool { } } -/// Check and display shim mode. Returns the mode and any found system node path. -async fn check_shim_mode(scope: EnvScope) -> (config::Config, Option) { +/// Check and display shim mode. Returns the mode and any found system node path, +/// or `None` when the config cannot be read. +async fn check_shim_mode(scope: EnvScope) -> Option<(config::Config, Option)> { let config = match load_config().await { Ok(c) => c, Err(e) => { print_check( - &style(output::WARN_SIGN).yellow().to_string(), - "Node.js", - &style(format!("config error: {e}")).yellow().to_string(), + &style(output::CROSS).red().to_string(), + "Config", + &style(format!("failed to read: {e}")).red().to_string(), ); - return (config::Config::default(), None); + if let Ok(path) = config::get_config_path() { + print_hint(&format!( + "Fix or remove {}.", + abbreviate_home(&path.as_path().display().to_string()) + )); + } + return None; } }; @@ -328,7 +345,7 @@ async fn check_shim_mode(scope: EnvScope) -> (config::Config, Option bool { }; // Show which tool would be executed for each shim + let mut ok = vp_path.is_some(); for tool in selected_shim_tools(scope) { if let Some(tool_path) = find_in_path(tool) { let expected_dir = super::setup::shim_dir(&settings, tool); @@ -594,11 +612,16 @@ async fn check_path(scope: EnvScope) -> bool { ); } } else { - print_check(" ", tool, "not found"); + print_check( + &style(output::CROSS).red().to_string(), + tool, + &style("not found").red().to_string(), + ); + ok = false; } } - vp_path.is_some() + ok } /// Find an executable in PATH. @@ -778,19 +801,29 @@ fn format_version_source(source: &str, source_path: Option<&vt_path::AbsolutePat } } -/// Check current directory version resolution. +/// Check current directory version resolution. Returns whether the check passed +/// and the managed resolution, if any. async fn check_current_resolution( cwd: &AbsolutePathBuf, shim_mode: ShimMode, system_node_path: Option, -) -> Option { +) -> (bool, Option) { print_check(" ", "Directory", &cwd.as_path().display().to_string()); // In system-first mode, show system Node.js info instead of managed resolution if shim_mode == ShimMode::SystemFirst { if let Some(system_node) = system_node_path { - let version = get_node_version(&system_node).await; print_check(" ", "Source", "system PATH"); + let Some(version) = try_get_tool_version(&system_node).await else { + print_check( + &style(output::CROSS).red().to_string(), + "Node binary", + &style(format!("{} (could not execute)", system_node.as_path().display())) + .red() + .to_string(), + ); + return (false, None); + }; print_check(" ", "Version", &style(&version).green().bright().to_string()); print_check( &style(output::CHECK).green().to_string(), @@ -805,7 +838,7 @@ async fn check_current_resolution( ); print_hint("Install Node.js or run 'vp env on' to use managed Node.js."); } - return None; + return (true, None); } match resolve_version(cwd).await { @@ -838,7 +871,7 @@ async fn check_current_resolution( ); print_hint("Version will be downloaded on first use."); } - Some(resolution) + (true, Some(resolution)) } Err(e) => { print_check( @@ -846,20 +879,11 @@ async fn check_current_resolution( "Resolution", &style(format!("failed: {e}")).red().to_string(), ); - None + (false, None) } } } -/// Get the version string from a Node.js binary. -async fn get_node_version(node_path: &vt_path::AbsolutePath) -> String { - get_tool_version(node_path).await -} - -async fn get_tool_version(tool_path: &vt_path::AbsolutePath) -> String { - try_get_tool_version(tool_path).await.unwrap_or_else(|| "unknown".to_string()) -} - async fn try_get_tool_version(tool_path: &vt_path::AbsolutePath) -> Option { let output = tokio::process::Command::new(tool_path.as_path()).arg("--version").output().await.ok()?; From 2ab3b850d9ee89912f763c20ecc7b4a78bf0bf1f Mon Sep 17 00:00:00 2001 From: Liang Date: Fri, 23 Oct 2026 22:43:46 +0800 Subject: [PATCH 2/2] fix(env): name the unreadable config in doctor and drop the misleading summary hint --- .../command_env_doctor_broken_config.md | 7 +- .../vp_global_cli/src/commands/env/doctor.rs | 65 ++++++++++--------- rfcs/env-command.md | 4 +- 3 files changed, 40 insertions(+), 36 deletions(-) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_doctor_broken_config/snapshots/command_env_doctor_broken_config.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_doctor_broken_config/snapshots/command_env_doctor_broken_config.md index 873d097c83..b08ff5faf1 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_doctor_broken_config/snapshots/command_env_doctor_broken_config.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_doctor_broken_config/snapshots/command_env_doctor_broken_config.md @@ -25,8 +25,7 @@ Installation ✓ State dir ~/.vite-plus Configuration - ✗ Config failed to read: JSON error: key must be a string at line 1 column 3 - note: Fix or remove ~/.vite-plus/config.json. + ✗ Config ~/.vite-plus/config.json (could not read) ✓ IDE integration env sourced in ~/.profile PATH @@ -35,10 +34,10 @@ PATH Node.js Resolution Directory - ✗ Resolution failed: JSON error: key must be a string at line 1 column 3 + ✗ Resolution ~/.vite-plus/config.json (could not read) Package Manager Resolution Package manager not selected -✗ Some issues found. Run the suggested commands to fix them. +✗ Some issues found. ``` diff --git a/crates/vp_global_cli/src/commands/env/doctor.rs b/crates/vp_global_cli/src/commands/env/doctor.rs index 6676e88c86..54f5e4c9d6 100644 --- a/crates/vp_global_cli/src/commands/env/doctor.rs +++ b/crates/vp_global_cli/src/commands/env/doctor.rs @@ -106,13 +106,10 @@ pub async fn execute(cwd: AbsolutePathBuf, scope: Option) -> Result result, - None => { - has_errors = true; - (config::Config::default(), None) - } - }; + let shim_mode = check_shim_mode(scope).await; + let config_readable = shim_mode.is_some(); + has_errors |= !config_readable; + let (environment_config, system_node_path) = shim_mode.unwrap_or_default(); // Check env sourcing: IDE-relevant profiles first, then all shell profiles let env_status = cfg!(not(windows)).then(check_env_sourcing); @@ -131,9 +128,13 @@ pub async fn execute(cwd: AbsolutePathBuf, scope: Option) -> Result) -> Result bool { async fn check_shim_mode(scope: EnvScope) -> Option<(config::Config, Option)> { let config = match load_config().await { Ok(c) => c, - Err(e) => { - print_check( - &style(output::CROSS).red().to_string(), - "Config", - &style(format!("failed to read: {e}")).red().to_string(), - ); - if let Ok(path) = config::get_config_path() { - print_hint(&format!( - "Fix or remove {}.", - abbreviate_home(&path.as_path().display().to_string()) - )); - } + Err(_) => { + print_check(&style(output::CROSS).red().to_string(), "Config", &unreadable_config()); return None; } }; @@ -348,6 +337,13 @@ async fn check_shim_mode(scope: EnvScope) -> Option<(config::Config, Option String { + let path = config::get_config_path() + .map(|path| abbreviate_home(&path.as_path().display().to_string())) + .unwrap_or_default(); + style(format!("{path} (could not read)")).red().to_string() +} + async fn check_package_manager_session_override() { for kind in package_manager::ALL_PACKAGE_MANAGERS { let environment = package_manager::environment_version(kind); @@ -807,6 +803,7 @@ async fn check_current_resolution( cwd: &AbsolutePathBuf, shim_mode: ShimMode, system_node_path: Option, + config_readable: bool, ) -> (bool, Option) { print_check(" ", "Directory", &cwd.as_path().display().to_string()); @@ -874,11 +871,19 @@ async fn check_current_resolution( (true, Some(resolution)) } Err(e) => { - print_check( - &style(output::CROSS).red().to_string(), - "Resolution", - &style(format!("failed: {e}")).red().to_string(), - ); + // Without a `VP_NODE_VERSION` override or a project version, resolution reads the global config. + let reads_config = !config_readable + && vp_shared::EnvConfig::get() + .node_version + .as_deref() + .is_none_or(|version| version.trim().is_empty()) + && matches!(config::resolve_project_version_source(cwd, false).await, Ok(None)); + let message = if reads_config { + unreadable_config() + } else { + style(format!("failed: {e}")).red().to_string() + }; + print_check(&style(output::CROSS).red().to_string(), "Resolution", &message); (false, None) } } diff --git a/rfcs/env-command.md b/rfcs/env-command.md index f6084b6c15..d6b399c3ef 100644 --- a/rfcs/env-command.md +++ b/rfcs/env-command.md @@ -831,7 +831,7 @@ PATH ... -✗ Some issues found. Run the suggested commands to fix them. +✗ Some issues found. ``` ## User Experience @@ -1037,7 +1037,7 @@ IDE Setup . "$HOME/.vite-plus/env" Then restart your IDE to apply changes. -✗ Some issues found. Run the suggested commands to fix them. +✗ Some issues found. ``` ## Shell Configuration Reference