diff --git a/crates/vp_global_cli/src/cli.rs b/crates/vp_global_cli/src/cli.rs index 306b981a0f..6216fef327 100644 --- a/crates/vp_global_cli/src/cli.rs +++ b/crates/vp_global_cli/src/cli.rs @@ -1092,7 +1092,7 @@ pub async fn run_command_with_options( } else { command_with_help_with_options(render_options).print_help().ok(); } - println!(); + vp_shared::output::print_stdout_line(format_args!("")); // Return a successful exit status since help was requested implicitly return Ok(std::process::ExitStatus::default()); }; diff --git a/crates/vp_global_cli/src/commands/env/current.rs b/crates/vp_global_cli/src/commands/env/current.rs index 9d671ee639..3cd6b06e1a 100644 --- a/crates/vp_global_cli/src/commands/env/current.rs +++ b/crates/vp_global_cli/src/commands/env/current.rs @@ -47,11 +47,15 @@ struct PackageManagerInfo { } fn print_rows(title: &str, rows: &[(String, String)]) { - println!("{}", help::render_heading(title)); + vp_shared::output::print_stdout_line(format_args!("{}", help::render_heading(title))); let label_width = rows.iter().map(|(label, _)| label.chars().count()).max().unwrap_or(0); for (label, value) in rows { let padding = " ".repeat(label_width.saturating_sub(label.chars().count())); - println!(" {}{} {value}", help::accent(label), padding); + vp_shared::output::print_stdout_line(format_args!( + " {}{} {value}", + help::accent(label), + padding + )); } } @@ -108,7 +112,10 @@ pub async fn execute( }; if json { - println!("{}", serde_json::to_string_pretty(&CurrentEnvInfo { node, package_manager })?); + vp_shared::output::print_stdout_line(format_args!( + "{}", + serde_json::to_string_pretty(&CurrentEnvInfo { node, package_manager })? + )); return Ok(ExitStatus::default()); } @@ -126,7 +133,7 @@ pub async fn execute( } if let Some(package_manager) = package_manager { if scope.includes_node() { - println!(); + vp_shared::output::print_stdout_line(format_args!("")); } let mut rows = vec![ ("Name".into(), package_manager.name), diff --git a/crates/vp_global_cli/src/commands/env/default.rs b/crates/vp_global_cli/src/commands/env/default.rs index 74bbf31627..afa7fc672c 100644 --- a/crates/vp_global_cli/src/commands/env/default.rs +++ b/crates/vp_global_cli/src/commands/env/default.rs @@ -29,7 +29,9 @@ pub async fn execute(values: Vec, unset: bool) -> Result, unset: bool) -> Result Result { match config.default_node_version.as_deref() { Some(version) => { has_configured_default = true; - println!("Default Node.js version: {version}"); + vp_shared::output::print_stdout_line(format_args!( + "Default Node.js version: {version}" + )); if matches!(version, "lts" | "latest") { let provider = vp_js_runtime::NodeProvider::new(); if let Ok(resolved) = super::config::resolve_version_alias(version, &provider).await { - println!(" Currently resolves to: {resolved}"); + vp_shared::output::print_stdout_line(format_args!( + " Currently resolves to: {resolved}" + )); } } } @@ -93,13 +99,17 @@ async fn show_default(scope: EnvScope) -> Result { let provider = vp_js_runtime::NodeProvider::new(); match provider.resolve_latest_version().await { Ok(version) => { - println!( + vp_shared::output::print_stdout_line(format_args!( "No default Node.js version configured. Using latest LTS ({version})." - ); + )); } - Err(_) => println!("No default Node.js version configured."), + Err(_) => vp_shared::output::print_stdout_line(format_args!( + "No default Node.js version configured." + )), } - println!(" Run 'vp env default ' to set a default."); + vp_shared::output::print_stdout_line(format_args!( + " Run 'vp env default ' to set a default." + )); } } } @@ -115,20 +125,27 @@ async fn show_default(scope: EnvScope) -> Result { .collect::>(); if configured.is_empty() { match scope { - EnvScope::PackageManager(kind) => { - println!("Default {kind} version: not configured") - } - _ => println!("Package manager defaults: not configured"), + EnvScope::PackageManager(kind) => vp_shared::output::print_stdout_line( + format_args!("Default {kind} version: not configured"), + ), + _ => vp_shared::output::print_stdout_line(format_args!( + "Package manager defaults: not configured" + )), } } else { for (package_manager, version) in configured { has_configured_default = true; - println!("Default {package_manager} version: {version}"); + vp_shared::output::print_stdout_line(format_args!( + "Default {package_manager} version: {version}" + )); } } } if has_configured_default { - println!(" Set via: {}", get_config_path()?.as_path().display()); + vp_shared::output::print_stdout_line(format_args!( + " Set via: {}", + get_config_path()?.as_path().display() + )); } Ok(ExitStatus::default()) } diff --git a/crates/vp_global_cli/src/commands/env/doctor.rs b/crates/vp_global_cli/src/commands/env/doctor.rs index 87bbc29bbe..8f16349e49 100644 --- a/crates/vp_global_cli/src/commands/env/doctor.rs +++ b/crates/vp_global_cli/src/commands/env/doctor.rs @@ -43,8 +43,8 @@ const KEY_WIDTH: usize = 18; /// Print a section header (bold, with blank line before). fn print_section(name: &str) { - println!(); - println!("{}", style(&name).bold()); + vp_shared::output::print_stdout_line(format_args!("")); + vp_shared::output::print_stdout_line(format_args!("{}", style(&name).bold())); } /// Print an aligned key-value line with a status indicator. @@ -53,11 +53,11 @@ fn print_section(name: &str) { /// Use `" "` for informational lines with no status. fn print_check(status: &str, key: &str, value: &str) { if status.trim().is_empty() { - println!(" {key:) -> Result) -> Result bool { &style("not in PATH").red().to_string(), ); print_hint(&format!("Expected: {bin_display}")); - println!(); + vp_shared::output::print_stdout_line(format_args!("")); print_path_fix(&vp_shared::EnvConfig::get().dirs.config); return false; } @@ -630,28 +636,48 @@ fn print_path_fix(env_dir: &vt_path::AbsolutePath) { env_path }; - println!(" {}", style("Add to your shell profile (~/.zshrc, ~/.bashrc, etc.):").dim()); - println!(); - println!(" . \"{env_path}/env\""); - println!(); - println!(" {}", style("For fish shell, add to ~/.config/fish/config.fish:").dim()); - println!(); - println!(" source \"{env_path}/env.fish\""); - println!(); - println!(" {}", style("For Nushell, add to ~/.config/nushell/config.nu:").dim()); - println!(); - println!(" source '{env_path}/env.nu'"); - println!(); - println!(" {}", style("Then restart your terminal.").dim()); + vp_shared::output::print_stdout_line(format_args!( + " {}", + style("Add to your shell profile (~/.zshrc, ~/.bashrc, etc.):").dim() + )); + vp_shared::output::print_stdout_line(format_args!("")); + vp_shared::output::print_stdout_line(format_args!(" . \"{env_path}/env\"")); + vp_shared::output::print_stdout_line(format_args!("")); + vp_shared::output::print_stdout_line(format_args!( + " {}", + style("For fish shell, add to ~/.config/fish/config.fish:").dim() + )); + vp_shared::output::print_stdout_line(format_args!("")); + vp_shared::output::print_stdout_line(format_args!(" source \"{env_path}/env.fish\"")); + vp_shared::output::print_stdout_line(format_args!("")); + vp_shared::output::print_stdout_line(format_args!( + " {}", + style("For Nushell, add to ~/.config/nushell/config.nu:").dim() + )); + vp_shared::output::print_stdout_line(format_args!("")); + vp_shared::output::print_stdout_line(format_args!(" source '{env_path}/env.nu'")); + vp_shared::output::print_stdout_line(format_args!("")); + vp_shared::output::print_stdout_line(format_args!( + " {}", + style("Then restart your terminal.").dim() + )); } #[cfg(windows)] { let _ = env_dir; - println!(" {}", style("Add the bin directory to your PATH via:").dim()); - println!(" System Properties -> Environment Variables -> Path"); - println!(); - println!(" {}", style("Then restart your terminal.").dim()); + vp_shared::output::print_stdout_line(format_args!( + " {}", + style("Add the bin directory to your PATH via:").dim() + )); + vp_shared::output::print_stdout_line(format_args!( + " System Properties -> Environment Variables -> Path" + )); + vp_shared::output::print_stdout_line(format_args!("")); + vp_shared::output::print_stdout_line(format_args!( + " {}", + style("Then restart your terminal.").dim() + )); } } @@ -705,30 +731,48 @@ fn print_ide_setup_guidance(env_dir: &vt_path::AbsolutePath) { "", &style("GUI applications may not see shell PATH changes.").yellow().to_string(), ); - println!(); + vp_shared::output::print_stdout_line(format_args!("")); #[cfg(target_os = "macos")] { - println!(" {}", style("macOS:").dim()); - println!(" {}", style("Add to ~/.zshenv or ~/.profile:").dim()); - println!(" . \"{env_path}/env\""); - println!(" {}", style("Then restart your IDE to apply changes.").dim()); + vp_shared::output::print_stdout_line(format_args!(" {}", style("macOS:").dim())); + vp_shared::output::print_stdout_line(format_args!( + " {}", + style("Add to ~/.zshenv or ~/.profile:").dim() + )); + vp_shared::output::print_stdout_line(format_args!(" . \"{env_path}/env\"")); + vp_shared::output::print_stdout_line(format_args!( + " {}", + style("Then restart your IDE to apply changes.").dim() + )); } #[cfg(target_os = "linux")] { - println!(" {}", style("Linux:").dim()); - println!(" {}", style("Add to ~/.profile:").dim()); - println!(" . \"{env_path}/env\""); - println!(" {}", style("Then log out and log back in for changes to take effect.").dim()); + vp_shared::output::print_stdout_line(format_args!(" {}", style("Linux:").dim())); + vp_shared::output::print_stdout_line(format_args!( + " {}", + style("Add to ~/.profile:").dim() + )); + vp_shared::output::print_stdout_line(format_args!(" . \"{env_path}/env\"")); + vp_shared::output::print_stdout_line(format_args!( + " {}", + style("Then log out and log back in for changes to take effect.").dim() + )); } // Fallback for other Unix platforms #[cfg(not(any(target_os = "macos", target_os = "linux")))] { - println!(" {}", style("Add to your shell profile:").dim()); - println!(" . \"{env_path}/env\""); - println!(" {}", style("Then restart your IDE to apply changes.").dim()); + vp_shared::output::print_stdout_line(format_args!( + " {}", + style("Add to your shell profile:").dim() + )); + vp_shared::output::print_stdout_line(format_args!(" . \"{env_path}/env\"")); + vp_shared::output::print_stdout_line(format_args!( + " {}", + style("Then restart your IDE to apply changes.").dim() + )); } } diff --git a/crates/vp_global_cli/src/commands/env/lifecycle.rs b/crates/vp_global_cli/src/commands/env/lifecycle.rs index 7e30f020a0..29c27d5893 100644 --- a/crates/vp_global_cli/src/commands/env/lifecycle.rs +++ b/crates/vp_global_cli/src/commands/env/lifecycle.rs @@ -52,9 +52,9 @@ pub(crate) async fn install( } }; if let Some((version, from_session_override)) = resolved { - println!("Installing Node.js v{version}..."); + vp_shared::output::print_stdout_line(format_args!("Installing Node.js v{version}...")); vp_js_runtime::download_runtime(vp_js_runtime::JsRuntimeType::Node, &version).await?; - println!("Installed Node.js v{version}"); + vp_shared::output::print_stdout_line(format_args!("Installed Node.js v{version}")); if from_session_override { eprintln!("Note: Installed from session override."); eprintln!("Run `vp env use --unset` to revert to project version resolution."); @@ -83,9 +83,9 @@ pub(crate) async fn install( }) }; if let Some((kind, version, hash)) = requested { - println!("Installing {kind} v{version}..."); + vp_shared::output::print_stdout_line(format_args!("Installing {kind} v{version}...")); download_package_manager(kind, &version, hash.as_deref()).await?; - println!("Installed {kind} v{version}"); + vp_shared::output::print_stdout_line(format_args!("Installed {kind} v{version}")); } } @@ -133,7 +133,7 @@ pub(crate) async fn uninstall(specs: Vec) -> Result { } for (label, target) in targets { tokio::fs::remove_dir_all(target.as_path()).await?; - println!("Uninstalled {label}"); + vp_shared::output::print_stdout_line(format_args!("Uninstalled {label}")); } Ok(ExitStatus::default()) } diff --git a/crates/vp_global_cli/src/commands/env/list.rs b/crates/vp_global_cli/src/commands/env/list.rs index 02488bd727..ab17e5330c 100644 --- a/crates/vp_global_cli/src/commands/env/list.rs +++ b/crates/vp_global_cli/src/commands/env/list.rs @@ -118,10 +118,10 @@ pub async fn execute( }; if json { - println!( + vp_shared::output::print_stdout_line(format_args!( "{}", serde_json::to_string_pretty(&InstalledEnvironmentJson { node, package_managers })? - ); + )); return Ok(ExitStatus::default()); } @@ -132,7 +132,7 @@ pub async fn execute( for kind in package_manager::selected(scope) { let name = kind.to_string(); if scope.includes_node() || kind != PackageManagerType::Npm { - println!(); + vp_shared::output::print_stdout_line(format_args!("")); } print_section( package_manager::title(kind), @@ -164,9 +164,9 @@ pub(super) fn list_complete_package_manager_versions( } fn print_section(title: &str, versions: &[InstalledVersionJson], node: bool) { - println!("{title}"); + vp_shared::output::print_stdout_line(format_args!("{title}")); if versions.is_empty() { - println!(" No versions installed."); + vp_shared::output::print_stdout_line(format_args!(" No versions installed.")); return; } let colorize = use_color(); @@ -188,9 +188,12 @@ fn print_section(title: &str, versions: &[InstalledVersionJson], node: bool) { let display = if node { format!("v{}", version.version) } else { version.version.clone() }; let line = format!("* {display}"); if version.current && colorize { - println!(" {}{suffix}", style(&line).blue().bright()); + vp_shared::output::print_stdout_line(format_args!( + " {}{suffix}", + style(&line).blue().bright() + )); } else { - println!(" {line}{suffix}"); + vp_shared::output::print_stdout_line(format_args!(" {line}{suffix}")); } } } diff --git a/crates/vp_global_cli/src/commands/env/mod.rs b/crates/vp_global_cli/src/commands/env/mod.rs index f270482900..876ddb46b4 100644 --- a/crates/vp_global_cli/src/commands/env/mod.rs +++ b/crates/vp_global_cli/src/commands/env/mod.rs @@ -213,8 +213,10 @@ async fn print_env(cwd: AbsolutePathBuf, scope: Option) -> Result) -> Result { EnvScope::PackageManagers => "Package-manager management".into(), EnvScope::PackageManager(package_manager) => format!("{package_manager} management"), }; - println!("\u{2713} {component} set to system-first."); - println!(); - println!( + vp_shared::output::print_stdout_line(format_args!("\u{2713} {component} set to system-first.")); + vp_shared::output::print_stdout_line(format_args!("")); + vp_shared::output::print_stdout_line(format_args!( "Selected commands and shims will now prefer system tools, falling back to managed tools." - ); - println!(); - println!("Run {} to always use Vite+ managed tools.", help::accent_command("vp env on")); + )); + vp_shared::output::print_stdout_line(format_args!("")); + vp_shared::output::print_stdout_line(format_args!( + "Run {} to always use Vite+ managed tools.", + help::accent_command("vp env on") + )); Ok(ExitStatus::default()) } diff --git a/crates/vp_global_cli/src/commands/env/on.rs b/crates/vp_global_cli/src/commands/env/on.rs index 67a1205607..a79b29e9bd 100644 --- a/crates/vp_global_cli/src/commands/env/on.rs +++ b/crates/vp_global_cli/src/commands/env/on.rs @@ -32,11 +32,16 @@ pub async fn execute(scope: Option) -> Result { EnvScope::PackageManagers => "Package-manager management".into(), EnvScope::PackageManager(package_manager) => format!("{package_manager} management"), }; - println!("\u{2713} {component} set to managed."); - println!(); - println!("Selected commands and shims will now use Vite+ managed tools."); - println!(); - println!("Run {} to prefer system tools instead.", help::accent_command("vp env off")); + vp_shared::output::print_stdout_line(format_args!("\u{2713} {component} set to managed.")); + vp_shared::output::print_stdout_line(format_args!("")); + vp_shared::output::print_stdout_line(format_args!( + "Selected commands and shims will now use Vite+ managed tools." + )); + vp_shared::output::print_stdout_line(format_args!("")); + vp_shared::output::print_stdout_line(format_args!( + "Run {} to prefer system tools instead.", + help::accent_command("vp env off") + )); Ok(ExitStatus::default()) } diff --git a/crates/vp_global_cli/src/commands/env/pin.rs b/crates/vp_global_cli/src/commands/env/pin.rs index f0e3ac960b..2970ded82b 100644 --- a/crates/vp_global_cli/src/commands/env/pin.rs +++ b/crates/vp_global_cli/src/commands/env/pin.rs @@ -53,7 +53,7 @@ pub async fn execute( if specs.is_empty() { show_pinned(&cwd).await?; - println!(); + vp_shared::output::print_stdout_line(format_args!("")); return show_package_manager_pin(&cwd).await; } @@ -99,17 +99,17 @@ pub async fn execute( async fn show_package_manager_pin(cwd: &AbsolutePathBuf) -> Result { match resolve_package_manager_from_package_json(cwd)? { Some(resolution) => { - println!( + vp_shared::output::print_stdout_line(format_args!( "Pinned package manager: {}@{}", resolution.package_manager_type, resolution.version - ); - println!( + )); + vp_shared::output::print_stdout_line(format_args!( " Source: {} ({})", resolution.source_path.as_path().display(), resolution.source - ); + )); } - None => println!("No package manager pinned."), + None => vp_shared::output::print_stdout_line(format_args!("No package manager pinned.")), } Ok(ExitStatus::default()) } @@ -122,18 +122,21 @@ async fn show_pinned(cwd: &AbsolutePathBuf) -> Result { if tokio::fs::try_exists(&node_version_path).await.unwrap_or(false) { let content = tokio::fs::read_to_string(&node_version_path).await?; let version = content.trim(); - println!("Pinned version: {version}"); - println!(" Source: {}", node_version_path.as_path().display()); + vp_shared::output::print_stdout_line(format_args!("Pinned version: {version}")); + vp_shared::output::print_stdout_line(format_args!( + " Source: {}", + node_version_path.as_path().display() + )); return Ok(ExitStatus::default()); } // Check devEngines.runtime in the current directory's package.json if let Some(version) = read_dev_engines_node_version(cwd).await { - println!("Pinned version: {version}"); - println!( + vp_shared::output::print_stdout_line(format_args!("Pinned version: {version}")); + vp_shared::output::print_stdout_line(format_args!( " Source: {} (devEngines.runtime)", cwd.join(PACKAGE_JSON_FILE).as_path().display() - ); + )); return Ok(ExitStatus::default()); } @@ -141,26 +144,34 @@ async fn show_pinned(cwd: &AbsolutePathBuf) -> Result { if resolution.source == VersionSource::NvmrcFile && resolution.project_root.as_ref() == Some(cwd) { - println!("Pinned version: {}", resolution.version); - println!(" Source: {}", cwd.join(NVMRC_FILE).as_path().display()); + vp_shared::output::print_stdout_line(format_args!( + "Pinned version: {}", + resolution.version + )); + vp_shared::output::print_stdout_line(format_args!( + " Source: {}", + cwd.join(NVMRC_FILE).as_path().display() + )); return Ok(ExitStatus::default()); } if resolution.source == VersionSource::EnginesNode { let path = resolution.source_path.unwrap_or_else(|| cwd.join(PACKAGE_JSON_FILE)); - println!("No version pinned."); - println!( + vp_shared::output::print_stdout_line(format_args!("No version pinned.")); + vp_shared::output::print_stdout_line(format_args!( " Node.js constraint: {} from {} (engines.node)", resolution.version, path.as_path().display() - ); + )); return Ok(ExitStatus::default()); } } // Check for inherited version from parent directories if let Some((version, source)) = find_inherited_version(cwd).await? { - println!("No version pinned in current directory."); - println!(" Inherited: {version} from {source}"); + vp_shared::output::print_stdout_line(format_args!( + "No version pinned in current directory." + )); + vp_shared::output::print_stdout_line(format_args!(" Inherited: {version} from {source}")); return Ok(ExitStatus::default()); } @@ -169,12 +180,17 @@ async fn show_pinned(cwd: &AbsolutePathBuf) -> Result { match config.default_node_version { Some(version) => { let config_path = get_config_path()?; - println!("No version pinned."); - println!(" Using default: {version} (from {})", config_path.as_path().display()); + vp_shared::output::print_stdout_line(format_args!("No version pinned.")); + vp_shared::output::print_stdout_line(format_args!( + " Using default: {version} (from {})", + config_path.as_path().display() + )); } None => { - println!("No version pinned."); - println!(" Run 'vp env pin ' to pin a version."); + vp_shared::output::print_stdout_line(format_args!("No version pinned.")); + vp_shared::output::print_stdout_line(format_args!( + " Run 'vp env pin ' to pin a version." + )); } } @@ -349,7 +365,7 @@ fn confirm_overwrite_pin( force: bool, ) -> Result { if existing_version == resolved_version { - println!("Already pinned to {resolved_version}"); + vp_shared::output::print_stdout_line(format_args!("Already pinned to {resolved_version}")); return Ok(false); } if force { @@ -358,9 +374,9 @@ fn confirm_overwrite_pin( // Prompt for confirmation, defaulting to yes (the user explicitly asked to // pin a new version, so only an explicit "no" cancels) - print!("{source_label} {existing_version}"); - println!(); - print!("Overwrite with {resolved_version}? (Y/n): "); + vp_shared::output::print_stdout(format_args!("{source_label} {existing_version}")); + vp_shared::output::print_stdout_line(format_args!("")); + vp_shared::output::print_stdout(format_args!("Overwrite with {resolved_version}? (Y/n): ")); std::io::stdout().flush()?; let mut input = String::new(); @@ -368,7 +384,7 @@ fn confirm_overwrite_pin( let answer = input.trim(); if answer.eq_ignore_ascii_case("n") || answer.eq_ignore_ascii_case("no") { - println!("Cancelled."); + vp_shared::output::print_stdout_line(format_args!("Cancelled.")); return Ok(false); } Ok(true) @@ -416,7 +432,11 @@ async fn pin_node_version_file( tokio::fs::write(&node_version_path, format!("{resolved_version}\n")).await?; print_pin_success(input_version, resolved_version, was_alias); - println!(" Created {} in {}", NODE_VERSION_FILE, cwd.as_path().display()); + vp_shared::output::print_stdout_line(format_args!( + " Created {} in {}", + NODE_VERSION_FILE, + cwd.as_path().display() + )); // If a devEngines.runtime range is declared and no longer satisfied, offer to // sync it in interactive terminals and warn otherwise (rfcs/dev-engines.md) @@ -481,7 +501,10 @@ async fn pin_nvmrc_file( } tokio::fs::write(&path, content).await?; print_pin_success(input_version, resolved_version, was_alias); - println!(" Updated {NVMRC_FILE} in {}", cwd.as_path().display()); + vp_shared::output::print_stdout_line(format_args!( + " Updated {NVMRC_FILE} in {}", + cwd.as_path().display() + )); Ok(true) } @@ -512,7 +535,10 @@ async fn pin_dev_engines( write_dev_engines_node_version(cwd, resolved_version).await?; print_pin_success(input_version, resolved_version, was_alias); - println!(" Updated devEngines.runtime in {}", package_json_path.as_path().display()); + vp_shared::output::print_stdout_line(format_args!( + " Updated devEngines.runtime in {}", + package_json_path.as_path().display() + )); Ok(true) } @@ -550,10 +576,10 @@ async fn check_dev_engines_sync( } if interactive && !force { - print!( + vp_shared::output::print_stdout(format_args!( "devEngines.runtime (\"{declared}\") is no longer satisfied. Update it to \ - {resolved_version}? (y/n): " - ); + {resolved_version}? (y/n): " + )); std::io::stdout().flush()?; let mut input = String::new(); @@ -742,7 +768,9 @@ pub async fn do_unpin( let file = if target == PinTarget::Nvmrc { NVMRC_FILE } else { NODE_VERSION_FILE }; let path = cwd.join(file); if !tokio::fs::try_exists(&path).await.unwrap_or(false) { - println!("No {file} file in current directory."); + vp_shared::output::print_stdout_line(format_args!( + "No {file} file in current directory." + )); return Ok(ExitStatus::default()); } @@ -763,7 +791,9 @@ pub async fn do_unpin( cwd.join(PACKAGE_JSON_FILE).as_path().display() )); } else { - println!("No Node.js pin found in current directory."); + vp_shared::output::print_stdout_line(format_args!( + "No Node.js pin found in current directory." + )); } } PinTarget::PackageManager => { @@ -867,7 +897,9 @@ async fn pin_package_manager( }) .map_err(|error| Error::Other(format!("failed to update package.json: {error}").into()))?; if !changed { - println!("Already pinned to {package_manager}@{resolved}"); + vp_shared::output::print_stdout_line(format_args!( + "Already pinned to {package_manager}@{resolved}" + )); return Ok(ExitStatus::default()); } tokio::fs::write(&package_json_path, updated).await?; @@ -962,7 +994,9 @@ async fn unpin_package_manager( let root = workspace_root(cwd)?.unwrap_or_else(|| cwd.clone()); let package_json_path = root.join(PACKAGE_JSON_FILE); let Ok(content) = tokio::fs::read_to_string(&package_json_path).await else { - println!("No package manager pin found in current directory."); + vp_shared::output::print_stdout_line(format_args!( + "No package manager pin found in current directory." + )); return Ok(()); }; let effective = resolve_package_manager_from_package_json(&root)?; @@ -1006,7 +1040,9 @@ async fn unpin_package_manager( crate::shim::invalidate_cache(); output::success("Removed package-manager pin"); } else { - println!("No package manager pin found in current directory."); + vp_shared::output::print_stdout_line(format_args!( + "No package manager pin found in current directory." + )); } Ok(()) } diff --git a/crates/vp_global_cli/src/commands/env/use.rs b/crates/vp_global_cli/src/commands/env/use.rs index bb7160c7bb..6200611fa9 100644 --- a/crates/vp_global_cli/src/commands/env/use.rs +++ b/crates/vp_global_cli/src/commands/env/use.rs @@ -114,10 +114,16 @@ pub async fn execute( } if has_eval_wrapper() { if scope.includes_node() { - println!("{}", format_unset(&shell, VERSION_ENV_VAR)); + vp_shared::output::print_stdout_line(format_args!( + "{}", + format_unset(&shell, VERSION_ENV_VAR) + )); } for kind in package_manager::selected(scope) { - println!("{}", format_unset(&shell, package_manager::version_env_var(kind))); + vp_shared::output::print_stdout_line(format_args!( + "{}", + format_unset(&shell, package_manager::version_env_var(kind)) + )); } } else if !can_use_session_file() { print_windows_eval_wrapper_required(); @@ -233,22 +239,28 @@ pub async fn execute( if has_eval_wrapper() { if let Some((version, _)) = &node { config::delete_session_version().await?; - println!("{}", format_export(&shell, VERSION_ENV_VAR, version)); + vp_shared::output::print_stdout_line(format_args!( + "{}", + format_export(&shell, VERSION_ENV_VAR, version) + )); } if let Some((kind, version, _, hash)) = &package_manager { config::delete_session_package_manager(*kind).await?; - println!( + vp_shared::output::print_stdout_line(format_args!( "{}", format_export( &shell, package_manager::version_env_var(*kind), &package_manager_spec(version, hash.as_deref())? ) - ); + )); } else if uses_project_environment && scope.includes_package_managers() { for kind in package_manager::selected(scope) { config::delete_session_package_manager(kind).await?; - println!("{}", format_unset(&shell, package_manager::version_env_var(kind))); + vp_shared::output::print_stdout_line(format_args!( + "{}", + format_unset(&shell, package_manager::version_env_var(kind)) + )); } } } else if !can_use_session_file() { diff --git a/crates/vp_global_cli/src/commands/env/which.rs b/crates/vp_global_cli/src/commands/env/which.rs index 33d975b787..9bc8407cd2 100644 --- a/crates/vp_global_cli/src/commands/env/which.rs +++ b/crates/vp_global_cli/src/commands/env/which.rs @@ -42,7 +42,7 @@ pub async fn execute(cwd: AbsolutePathBuf, tool: &str) -> Result Result Result Result Result<(), Error> { } let json = serde_json::to_string_pretty(&output)?; - println!("{json}"); + vp_shared::output::print_stdout_line(format_args!("{json}")); Ok(()) } fn print_list(packages: &[OutdatedPackage], long: bool) { for (index, package) in packages.iter().enumerate() { if index > 0 { - println!(); + vp_shared::output::print_stdout_line(format_args!("")); } - println!("{} {}", style(&package.name).bold(), style("(global)").dim()); + vp_shared::output::print_stdout_line(format_args!( + "{} {}", + style(&package.name).bold(), + style("(global)").dim() + )); if package.wanted == package.latest { - println!( + vp_shared::output::print_stdout_line(format_args!( "{} {} {}", style(&package.current).dim(), style("=>").dim(), style(&package.wanted).bold() - ); + )); } else { - println!( + vp_shared::output::print_stdout_line(format_args!( "{} {} {} {}", style(&package.current).dim(), style("=>").dim(), style(&package.wanted).bold(), style(format!("(latest: {})", package.latest)).dim() - ); + )); } if long { - println!("{} {}", style("node").dim(), package.node); + vp_shared::output::print_stdout_line(format_args!( + "{} {}", + style("node").dim(), + package.node + )); if !package.bins.is_empty() { - println!("{} {}", style("bins").dim(), package.bins.join(", ")); + vp_shared::output::print_stdout_line(format_args!( + "{} {}", + style("bins").dim(), + package.bins.join(", ") + )); } } } @@ -341,28 +353,28 @@ fn print_table(packages: &[OutdatedPackage], long: bool) { let gap = 3; if long { - println!( + vp_shared::output::print_stdout_line(format_args!( "{:gap$}{:gap$}{:gap$}{:gap$}{:gap$}{}", col_pkg, "", col_current, "", col_wanted, "", col_latest, "", col_node, "", col_bins - ); - println!( + )); + vp_shared::output::print_stdout_line(format_args!( "{:gap$}{:gap$}{:gap$}{:gap$}{:gap$}{}", "---", "", "---", "", "---", "", "---", "", "---", "", "---" - ); + )); } else { - println!( + vp_shared::output::print_stdout_line(format_args!( "{:gap$}{:gap$}{:gap$}{}", col_pkg, "", col_current, "", col_wanted, "", col_latest - ); - println!( + )); + vp_shared::output::print_stdout_line(format_args!( "{:gap$}{:gap$}{:gap$}---", "---", "", "---", "", "---", "" - ); + )); } for package in packages { if long { - println!( + vp_shared::output::print_stdout_line(format_args!( "{}{:>gap$}{:gap$}{:gap$}{:gap$}{:gap$}{}", style(format!("{:gap$}{:gap$}{:gap$}{}", style(format!("{:) -> Result"); + vp_shared::output::print_stdout_line(format_args!("No global packages installed.")); + vp_shared::output::print_stdout_line(format_args!("")); + vp_shared::output::print_stdout_line(format_args!( + "Install packages with: vp install -g " + )); } return Ok(ExitStatus::default()); } if json { let json_output = serde_json::to_string_pretty(&packages).map_err(Error::JsonError)?; - println!("{json_output}"); + vp_shared::output::print_stdout_line(format_args!("{json_output}")); } else { let col_pkg = "Package"; let col_node = "Node version"; @@ -50,20 +57,26 @@ pub async fn execute(json: bool, pattern: Option<&str>) -> Resultgap$}{:gap$}{}", col_pkg, "", col_node, "", col_bins); - println!("{:gap$}{:gap$}{}", "---", "", "---", "", "---"); + vp_shared::output::print_stdout_line(format_args!( + "{:gap$}{:gap$}{}", + col_pkg, "", col_node, "", col_bins + )); + vp_shared::output::print_stdout_line(format_args!( + "{:gap$}{:gap$}{}", + "---", "", "---", "", "---" + )); for pkg in &packages { let name = format!("{:gap$}{:gap$}{}", style(&name).blue().bright(), "", pkg.platform.node, "", bins - ); + )); } } diff --git a/crates/vp_global_cli/src/commands/upgrade/mod.rs b/crates/vp_global_cli/src/commands/upgrade/mod.rs index 94ea88a930..051e2c7701 100644 --- a/crates/vp_global_cli/src/commands/upgrade/mod.rs +++ b/crates/vp_global_cli/src/commands/upgrade/mod.rs @@ -34,7 +34,7 @@ pub struct UpgradeOptions { } /// Execute the upgrade command. -#[allow(clippy::print_stdout, clippy::print_stderr)] +#[allow(clippy::print_stderr)] pub async fn execute(options: UpgradeOptions) -> Result { if options.background_check { crate::upgrade_check::run_background_check().await; @@ -91,10 +91,17 @@ pub async fn execute(options: UpgradeOptions) -> Result { // Step 4: Handle --check (report and exit) if options.check { if resolved.version == current_version { - println!("\n{} Already up to date ({})", style(output::CHECK).green(), current_version); + vp_shared::output::print_stdout_line(format_args!( + "\n{} Already up to date ({})", + style(output::CHECK).green(), + current_version + )); } else { - println!("Update available: {} \u{2192} {}", current_version, resolved.version); - println!("Run `vp upgrade` to update."); + vp_shared::output::print_stdout_line(format_args!( + "Update available: {} \u{2192} {}", + current_version, resolved.version + )); + vp_shared::output::print_stdout_line(format_args!("Run `vp upgrade` to update.")); } return Ok(ExitStatus::default()); } @@ -102,7 +109,11 @@ pub async fn execute(options: UpgradeOptions) -> Result { // Step 5: Handle already up-to-date if resolved.version == current_version && !options.force { if !options.silent { - println!("\n{} Already up to date ({})", style(output::CHECK).green(), current_version); + vp_shared::output::print_stdout_line(format_args!( + "\n{} Already up to date ({})", + style(output::CHECK).green(), + current_version + )); } return Ok(ExitStatus::default()); } @@ -186,7 +197,7 @@ pub async fn execute(options: UpgradeOptions) -> Result { } /// Core installation logic, separated for error cleanup. -#[allow(clippy::print_stdout, clippy::print_stderr)] +#[allow(clippy::print_stderr)] async fn install_platform_and_main( platform_data: &[u8], version_dir: &AbsolutePathBuf, @@ -240,24 +251,24 @@ async fn install_platform_and_main( } if !silent { - println!( + vp_shared::output::print_stdout_line(format_args!( "\n{} Updated vite-plus from {} {} {}", style(output::CHECK).green(), current_version, output::ARROW, new_version - ); - println!( + )); + vp_shared::output::print_stdout_line(format_args!( "\n Release notes: https://github.com/voidzero-dev/vite-plus/releases/tag/v{}", new_version - ); + )); } Ok(ExitStatus::default()) } /// Execute rollback to the previous version. -#[allow(clippy::print_stdout, clippy::print_stderr)] +#[allow(clippy::print_stderr)] async fn execute_rollback( install_dir: &AbsolutePathBuf, silent: bool, @@ -291,7 +302,11 @@ async fn execute_rollback( install::refresh_shims(install_dir).await?; if !silent { - println!("\n{} Rolled back to {}", style(output::CHECK).green(), previous); + vp_shared::output::print_stdout_line(format_args!( + "\n{} Rolled back to {}", + style(output::CHECK).green(), + previous + )); } Ok(ExitStatus::default()) diff --git a/crates/vp_global_cli/src/commands/vpx.rs b/crates/vp_global_cli/src/commands/vpx.rs index 8b56900cda..828213f830 100644 --- a/crates/vp_global_cli/src/commands/vpx.rs +++ b/crates/vp_global_cli/src/commands/vpx.rs @@ -62,7 +62,7 @@ pub async fn execute_vpx(args: &[String], cwd: &AbsolutePath) -> i32 { // Show help if flags.help { - println!("{VPX_HELP}"); + vp_shared::output::print_stdout_line(format_args!("{VPX_HELP}")); return 0; } diff --git a/crates/vp_global_cli/src/main.rs b/crates/vp_global_cli/src/main.rs index 4bcae58dea..441743a24c 100644 --- a/crates/vp_global_cli/src/main.rs +++ b/crates/vp_global_cli/src/main.rs @@ -9,8 +9,7 @@ clippy::disallowed_macros, clippy::disallowed_methods, clippy::disallowed_types, - clippy::print_stderr, - clippy::print_stdout + clippy::print_stderr )] mod cli; @@ -358,12 +357,36 @@ fn dump_dirs_from_env_config() -> bool { } use vp_shared::env_vars::dump_dirs; let dirs = &vp_shared::EnvConfig::get().dirs; - println!("{}\t{}", dump_dirs::LAYOUT, dirs.layout().as_str()); - println!("{}\t{}", dump_dirs::DATA, dirs.data.as_path().display()); - println!("{}\t{}", dump_dirs::BIN, dirs.bin.as_path().display()); - println!("{}\t{}", dump_dirs::CACHE, dirs.cache.as_path().display()); - println!("{}\t{}", dump_dirs::CONFIG, dirs.config.as_path().display()); - println!("{}\t{}", dump_dirs::STATE, dirs.state.as_path().display()); + vp_shared::output::print_stdout_line(format_args!( + "{}\t{}", + dump_dirs::LAYOUT, + dirs.layout().as_str() + )); + vp_shared::output::print_stdout_line(format_args!( + "{}\t{}", + dump_dirs::DATA, + dirs.data.as_path().display() + )); + vp_shared::output::print_stdout_line(format_args!( + "{}\t{}", + dump_dirs::BIN, + dirs.bin.as_path().display() + )); + vp_shared::output::print_stdout_line(format_args!( + "{}\t{}", + dump_dirs::CACHE, + dirs.cache.as_path().display() + )); + vp_shared::output::print_stdout_line(format_args!( + "{}\t{}", + dump_dirs::CONFIG, + dirs.config.as_path().display() + )); + vp_shared::output::print_stdout_line(format_args!( + "{}\t{}", + dump_dirs::STATE, + dirs.state.as_path().display() + )); true } @@ -371,7 +394,7 @@ fn dump_dirs_from_env_config() -> bool { async fn main() -> ExitCode { // Probe before tracing, directory resolution, or argument dispatch can emit output. if env::var_os(vp_shared::env_vars::VP_SELF_SETUP_SUPPORT_CHECK).is_some() { - println!("vite-plus-self-setup-v1"); + vp_shared::output::print_stdout_line(format_args!("vite-plus-self-setup-v1")); return ExitCode::SUCCESS; } @@ -404,7 +427,10 @@ async fn main() -> ExitCode { // Replace bash completion script to fix completion for items containing ':' if env::var_os("VP_COMPLETE").is_some_and(|shell| shell == "bash") && args.len() == 1 { - print!("{}", include_str!("../completion-register.bash")); + vp_shared::output::print_stdout(format_args!( + "{}", + include_str!("../completion-register.bash") + )); return ExitCode::SUCCESS; } diff --git a/crates/vp_global_cli/src/self_setup.rs b/crates/vp_global_cli/src/self_setup.rs index 6cca5a3c0b..c73293189c 100644 --- a/crates/vp_global_cli/src/self_setup.rs +++ b/crates/vp_global_cli/src/self_setup.rs @@ -136,12 +136,15 @@ fn print_shell_result(shell: &str) { ] { let value = path.to_string(); if shell == "powershell" { - println!( + vp_shared::output::print_stdout_line(format_args!( "$script:{powershell_name} = '{}'", setup::escape_powershell_single_quoted_string(&value) - ); + )); } else { - println!("{sh_name}=\"{}\"", setup::escape_posix_double_quoted_string(&value)); + vp_shared::output::print_stdout_line(format_args!( + "{sh_name}=\"{}\"", + setup::escape_posix_double_quoted_string(&value) + )); } } } diff --git a/crates/vp_shared/src/header.rs b/crates/vp_shared/src/header.rs index f4ec220b32..277cd2e863 100644 --- a/crates/vp_shared/src/header.rs +++ b/crates/vp_shared/src/header.rs @@ -8,7 +8,12 @@ //! - Stream-based response parsing (modelled after `terminal-colorsaurus`) //! - Gradient/fade generation and RGB ANSI coloring -use std::sync::{LazyLock, OnceLock}; +#![deny(clippy::print_stdout)] + +use std::{ + io, + sync::{LazyLock, OnceLock}, +}; #[cfg(unix)] use std::{ io::Write, @@ -557,8 +562,10 @@ pub fn print_header() { if !should_print_header() { return; } - println!("{}", vite_plus_header()); - println!(); + crate::output::print_and_flush( + &mut io::stdout().lock(), + &format!("{}\n\n", vite_plus_header()), + ); } #[cfg(all(test, unix))] diff --git a/crates/vp_shared/src/lib.rs b/crates/vp_shared/src/lib.rs index 807e258e94..5afb874d9a 100644 --- a/crates/vp_shared/src/lib.rs +++ b/crates/vp_shared/src/lib.rs @@ -1,11 +1,8 @@ //! Shared utilities for vite-plus crates -#![allow( - clippy::allow_attributes, - clippy::disallowed_macros, - clippy::disallowed_types, - clippy::print_stdout -)] +#![allow(clippy::allow_attributes)] +#![allow(clippy::disallowed_macros)] +#![allow(clippy::disallowed_types)] mod dirs; mod documentation; diff --git a/crates/vp_shared/src/output.rs b/crates/vp_shared/src/output.rs index 3154f52d5b..964d4c765f 100644 --- a/crates/vp_shared/src/output.rs +++ b/crates/vp_shared/src/output.rs @@ -4,7 +4,10 @@ //! consistent output across the entire CLI. Styling uses console's color detection //! for the stream receiving each message. +#![deny(clippy::print_stdout, clippy::print_stderr)] + use std::{ + fmt, io::{self, Write}, sync::atomic::{AtomicBool, Ordering}, }; @@ -38,6 +41,22 @@ pub fn print_and_flush(writer: &mut dyn Write, message: &str) { } } +fn print_line(writer: &mut dyn Write, message: &str) { + print_and_flush(writer, &format!("{message}\n")); +} + +/// Print formatted command output to stdout without a trailing newline. +pub fn print_stdout(arguments: fmt::Arguments<'_>) { + print_and_flush(&mut io::stdout().lock(), &arguments.to_string()); +} + +/// Print formatted command output to stdout with a trailing newline. +pub fn print_stdout_line(arguments: fmt::Arguments<'_>) { + let mut message = arguments.to_string(); + message.push('\n'); + print_and_flush(&mut io::stdout().lock(), &message); +} + fn fail_for_writer_error(error: io::Error) -> ! { panic!("failed writing command output: {error}"); } @@ -71,35 +90,49 @@ pub const WARN_SIGN: &str = "\u{26A0}"; pub const ARROW: &str = "\u{2192}"; /// Print an info message to stdout. -#[expect(clippy::print_stdout, clippy::print_stderr, clippy::disallowed_macros)] pub fn info(msg: &str) { if user_output_to_stderr() { - eprintln!("{} {msg}", style("info:").for_stderr().blue().bright().bold()); + print_line( + &mut io::stderr().lock(), + &format!("{} {msg}", style("info:").for_stderr().blue().bright().bold()), + ); } else { - println!("{} {msg}", style("info:").blue().bright().bold()); + print_line( + &mut io::stdout().lock(), + &format!("{} {msg}", style("info:").blue().bright().bold()), + ); } } /// Print a pass message to stdout using the same accent styling as info. -#[expect(clippy::print_stdout, clippy::print_stderr, clippy::disallowed_macros)] pub fn pass(msg: &str) { if user_output_to_stderr() { - eprintln!("{} {msg}", style("pass:").for_stderr().blue().bright().bold()); + print_line( + &mut io::stderr().lock(), + &format!("{} {msg}", style("pass:").for_stderr().blue().bright().bold()), + ); } else { - println!("{} {msg}", style("pass:").blue().bright().bold()); + print_line( + &mut io::stdout().lock(), + &format!("{} {msg}", style("pass:").blue().bright().bold()), + ); } } /// Print a warning message to stderr. -#[expect(clippy::print_stderr, clippy::disallowed_macros)] pub fn warn(msg: &str) { - eprintln!("{} {msg}", style("warn:").for_stderr().yellow().bold()); + print_line( + &mut io::stderr().lock(), + &format!("{} {msg}", style("warn:").for_stderr().yellow().bold()), + ); } /// Print an error message to stderr. -#[expect(clippy::print_stderr, clippy::disallowed_macros)] pub fn error(msg: &str) { - eprintln!("{} {msg}", style("error:").for_stderr().red().bold()); + print_line( + &mut io::stderr().lock(), + &format!("{} {msg}", style("error:").for_stderr().red().bold()), + ); } /// Print a note message to stderr (supplementary info). @@ -107,45 +140,46 @@ pub fn error(msg: &str) { /// A note explains the situation around a command rather than being part of /// its result, so it belongs on the diagnostic stream: piping stdout to a file /// or a parser keeps the command's own output intact. -#[expect(clippy::print_stderr, clippy::disallowed_macros)] pub fn note(msg: &str) { - eprintln!("{} {msg}", style("note:").for_stderr().dim().bold()); + print_line( + &mut io::stderr().lock(), + &format!("{} {msg}", style("note:").for_stderr().dim().bold()), + ); } /// Print a success line with checkmark to stdout. -#[expect(clippy::print_stdout, clippy::print_stderr, clippy::disallowed_macros)] pub fn success(msg: &str) { if user_output_to_stderr() { - eprintln!("{} {msg}", style(CHECK).for_stderr().green()); + print_line( + &mut io::stderr().lock(), + &format!("{} {msg}", style(CHECK).for_stderr().green()), + ); } else { - println!("{} {msg}", style(CHECK).green()); + print_line(&mut io::stdout().lock(), &format!("{} {msg}", style(CHECK).green())); } } /// Print a raw message to stdout with no prefix or formatting. -#[expect(clippy::print_stdout, clippy::print_stderr, clippy::disallowed_macros)] pub fn raw(msg: &str) { if user_output_to_stderr() { - eprintln!("{msg}"); + print_line(&mut io::stderr().lock(), msg); } else { - println!("{msg}"); + print_line(&mut io::stdout().lock(), msg); } } /// Print a raw message to stdout without a trailing newline. -#[expect(clippy::print_stdout, clippy::print_stderr, clippy::disallowed_macros)] pub fn raw_inline(msg: &str) { if user_output_to_stderr() { - eprint!("{msg}"); + print_and_flush(&mut io::stderr().lock(), msg); } else { - print!("{msg}"); + print_and_flush(&mut io::stdout().lock(), msg); } } /// Print a raw message to stderr with no prefix or formatting. -#[expect(clippy::print_stderr, clippy::disallowed_macros)] pub fn raw_stderr(msg: &str) { - eprintln!("{msg}"); + print_line(&mut io::stderr().lock(), msg); } #[cfg(test)]