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..b08ff5faf1 --- /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,43 @@ +# 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 ~/.vite-plus/config.json (could not read) + ✓ IDE integration env sourced in ~/.profile + +PATH + ✓ vp ~/.vite-plus/bin/vp + ✓ Shim dir ~/.vite-plus/bin + +Node.js Resolution + Directory + ✗ Resolution ~/.vite-plus/config.json (could not read) + +Package Manager Resolution + Package manager not selected + +✗ 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 e19d10f40a..54f5e4c9d6 100644 --- a/crates/vp_global_cli/src/commands/env/doctor.rs +++ b/crates/vp_global_cli/src/commands/env/doctor.rs @@ -106,7 +106,10 @@ pub async fn execute(cwd: AbsolutePathBuf, scope: Option) -> Result) -> Result) -> 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(), - ); - return (config::Config::default(), None); + Err(_) => { + print_check(&style(output::CROSS).red().to_string(), "Config", &unreadable_config()); + return None; } }; @@ -328,7 +334,14 @@ async fn check_shim_mode(scope: EnvScope) -> (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() { @@ -569,6 +582,7 @@ async fn check_path(scope: EnvScope) -> 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 +608,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 +797,30 @@ 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 { + config_readable: bool, +) -> (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 +835,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,28 +868,27 @@ async fn check_current_resolution( ); print_hint("Version will be downloaded on first use."); } - Some(resolution) + (true, Some(resolution)) } Err(e) => { - print_check( - &style(output::CROSS).red().to_string(), - "Resolution", - &style(format!("failed: {e}")).red().to_string(), - ); - None + // 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) } } } -/// 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()?; 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