From bd2ca3fd7ad19a3039648ab73b47ff81fbe3616b Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Tue, 22 Sep 2026 22:25:15 +0900 Subject: [PATCH 01/10] fix: add broken pipe-safe output helper --- crates/vp_shared/src/output.rs | 37 +++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/crates/vp_shared/src/output.rs b/crates/vp_shared/src/output.rs index 30cf4c6a0d..96412b647d 100644 --- a/crates/vp_shared/src/output.rs +++ b/crates/vp_shared/src/output.rs @@ -4,10 +4,32 @@ //! consistent output across the entire CLI. Styling uses console's color detection //! for the stream receiving each message. -use std::sync::atomic::{AtomicBool, Ordering}; +use std::{ + io::{self, Write}, + sync::atomic::{AtomicBool, Ordering}, +}; use console::style; +/// Write a message and flush it without panicking for expected output-stream errors. +/// +/// Use this for command output that can be piped to a reader which exits early. +pub fn print_and_flush(writer: &mut dyn Write, message: &str) { + writer.write_all(message.as_bytes()).or_else(check_for_writer_error).unwrap(); + writer.flush().or_else(check_for_writer_error).unwrap(); +} + +fn check_for_writer_error(error: io::Error) -> io::Result<()> { + if matches!( + error.kind(), + io::ErrorKind::Interrupted | io::ErrorKind::BrokenPipe | io::ErrorKind::WouldBlock + ) { + Ok(()) + } else { + Err(error) + } +} + /// When set, user-facing stdout output (info/pass/note/success/raw) is routed /// to stderr instead. Shim dispatch enables this once at entry: a shim's /// stdout belongs to the wrapped tool and must stay parseable. @@ -113,3 +135,16 @@ pub fn raw_inline(msg: &str) { pub fn raw_stderr(msg: &str) { eprintln!("{msg}"); } + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(unix)] + #[test] + fn print_and_flush_tolerates_a_closed_pipe() { + let (reader, writer) = nix::unistd::pipe().unwrap(); + drop(reader); + print_and_flush(&mut std::fs::File::from(writer), "output\n"); + } +} From 2bca0719e240421bbe3bc9921ee326dc7315693a Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Tue, 22 Sep 2026 22:25:30 +0900 Subject: [PATCH 02/10] fix: handle broken pipes in command output --- .../src/commands/env/list_remote.rs | 25 +++++++++++-------- crates/vp_global_cli/src/commands/env/mod.rs | 4 +-- crates/vp_global_cli/src/commands/version.rs | 17 +++++++------ 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/crates/vp_global_cli/src/commands/env/list_remote.rs b/crates/vp_global_cli/src/commands/env/list_remote.rs index a3ef2b0caf..7c07d236bd 100644 --- a/crates/vp_global_cli/src/commands/env/list_remote.rs +++ b/crates/vp_global_cli/src/commands/env/list_remote.rs @@ -1,4 +1,4 @@ -use std::{collections::BTreeMap, process::ExitStatus}; +use std::{collections::BTreeMap, io, process::ExitStatus}; use console::style; use futures::future::try_join_all; @@ -17,6 +17,10 @@ use crate::{cli::SortingMethod, error::Error}; const DEFAULT_MAJOR_VERSIONS: usize = 10; +fn print_stdout(message: &str) { + vp_shared::output::print_and_flush(&mut io::stdout().lock(), message); +} + #[derive(Serialize)] struct RemoteEnvironmentJson { #[serde(skip_serializing_if = "Option::is_none")] @@ -152,24 +156,25 @@ pub async fn execute( }); if json { - println!( - "{}", - serde_json::to_string_pretty(&RemoteEnvironmentJson { node, package_managers })? - ); + let output = + serde_json::to_string_pretty(&RemoteEnvironmentJson { node, package_managers })?; + print_stdout(&format!("{output}\n")); return Ok(ExitStatus::default()); } if let Some(node) = node { - println!("Node.js"); + print_stdout("Node.js\n"); print_node_versions(&node); } if let Some(mut package_managers) = package_managers { for kind in package_manager::selected(scope) { let name = kind.to_string(); let versions = package_managers.remove(&name).unwrap_or_default(); - println!(); - println!("{}", package_manager::title(kind)); + print_stdout(&format!("\n{}\n", package_manager::title(kind))); for entry in versions { - println!(" {}", format_package_manager_version(&entry, use_color())); + print_stdout(&format!( + " {}\n", + format_package_manager_version(&entry, use_color()) + )); } } } @@ -184,7 +189,7 @@ fn print_node_versions(versions: &[NodeVersionJson]) { let colorize = use_color(); for entry in versions { - println!(" {}", format_node_version(entry, colorize)); + print_stdout(&format!(" {}\n", format_node_version(entry, colorize))); } } diff --git a/crates/vp_global_cli/src/commands/env/mod.rs b/crates/vp_global_cli/src/commands/env/mod.rs index 3f568cc799..d2206e172a 100644 --- a/crates/vp_global_cli/src/commands/env/mod.rs +++ b/crates/vp_global_cli/src/commands/env/mod.rs @@ -24,7 +24,7 @@ mod unpin; mod r#use; mod which; -use std::process::ExitStatus; +use std::{io, process::ExitStatus}; #[cfg(windows)] pub(crate) use setup::{cleanup_legacy_windows_shim, get_trampoline_path, remove_or_rename_to_old}; @@ -41,7 +41,7 @@ fn print_env_header() { } fn print_env_clean_tip() { - vp_shared::output::raw(""); + vp_shared::output::print_and_flush(&mut io::stdout().lock(), "\n"); vp_shared::output::note( "Run `vp env clean` to free disk space from unused managed runtimes and package manager caches.", ); diff --git a/crates/vp_global_cli/src/commands/version.rs b/crates/vp_global_cli/src/commands/version.rs index 26cff89b7e..1364cbec1b 100644 --- a/crates/vp_global_cli/src/commands/version.rs +++ b/crates/vp_global_cli/src/commands/version.rs @@ -2,7 +2,7 @@ use std::{ collections::BTreeMap, - fs, + fs, io, path::{Path, PathBuf}, process::ExitStatus, }; @@ -121,12 +121,16 @@ fn resolve_tool_version( } } +fn print_stdout(message: &str) { + vp_shared::output::print_and_flush(&mut io::stdout().lock(), message); +} + fn print_rows(title: &str, rows: &[(&str, String)]) { - println!("{}", help::render_heading(title)); + print_stdout(&format!("{}\n", 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); + print_stdout(&format!(" {}{} {value}\n", help::accent(label), padding)); } } @@ -169,8 +173,7 @@ fn detect_system_node_version() -> Option { pub async fn execute(cwd: AbsolutePathBuf) -> Result { vp_shared::header::print_header(); - println!("vp v{}", env!("CARGO_PKG_VERSION")); - println!(); + print_stdout(concat!("vp v", env!("CARGO_PKG_VERSION"), "\n\n")); // Local vite-plus and tools let local = find_local_vite_plus(cwd.as_path()); @@ -178,7 +181,7 @@ pub async fn execute(cwd: AbsolutePathBuf) -> Result { "Local vite-plus", &[("vite-plus", format_version(local.as_ref().map(|pkg| pkg.version.clone())))], ); - println!(); + print_stdout("\n"); let manifest = local.as_ref().and_then(read_toolchain_manifest); let tool_rows = TOOL_SPECS @@ -189,7 +192,7 @@ pub async fn execute(cwd: AbsolutePathBuf) -> Result { }) .collect::>(); print_rows("Tools", &tool_rows); - println!(); + print_stdout("\n"); // Environment info let package_manager_info = find_workspace_root(&cwd) From 1dd7e797db14f4281613e508db0ace6aaaa835c8 Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Tue, 22 Sep 2026 22:25:48 +0900 Subject: [PATCH 03/10] test: cover commands with closed stdout --- .../src/bin/vpt/head_lines.rs | 51 +++++++++++++++++++ crates/vp_cli_snapshots/src/bin/vpt/main.rs | 4 +- .../tests/cli_snapshots/README.md | 4 +- .../command_env_list_remote/snapshots.toml | 9 ++++ .../command_env_list_remote_closed_pipe.md | 10 ++++ .../fixtures/command_version/snapshots.toml | 8 +++ .../snapshots/command_version_closed_pipe.md | 9 ++++ 7 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 crates/vp_cli_snapshots/src/bin/vpt/head_lines.rs create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_list_remote/snapshots/command_env_list_remote_closed_pipe.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_version/snapshots/command_version_closed_pipe.md diff --git a/crates/vp_cli_snapshots/src/bin/vpt/head_lines.rs b/crates/vp_cli_snapshots/src/bin/vpt/head_lines.rs new file mode 100644 index 0000000000..4e4e6bf89a --- /dev/null +++ b/crates/vp_cli_snapshots/src/bin/vpt/head_lines.rs @@ -0,0 +1,51 @@ +use std::{ + io::{Read, Write}, + process::{Command, Stdio}, +}; + +/// head-lines `` -- `` \[``...\] +/// +/// Reads and prints the first `` lines from the child's stdout, closes +/// the pipe, then exits with the child's exit code. +pub fn run(args: &[String]) -> Result<(), Box> { + let sep = args + .iter() + .position(|arg| arg == "--") + .ok_or("Usage: vpt head-lines -- [args...]")?; + if sep != 1 || args.len() <= sep + 1 { + return Err("Usage: vpt head-lines -- [args...]".into()); + } + + let count = args[0].parse::()?; + if count == 0 { + return Err("head-lines count must be greater than zero".into()); + } + + let cmd_args = &args[sep + 1..]; + let mut child = + Command::new(&cmd_args[0]).args(&cmd_args[1..]).stdout(Stdio::piped()).spawn()?; + let mut child_stdout = child.stdout.take().unwrap(); + let mut captured = Vec::new(); + let mut lines = 0; + + while lines < count { + let mut byte = [0]; + if child_stdout.read(&mut byte)? == 0 { + break; + } + captured.push(byte[0]); + if byte[0] == b'\n' { + lines += 1; + } + } + + // Closing the read end reproduces a consumer such as `head` exiting early. + drop(child_stdout); + let status = child.wait()?; + + let mut stdout = std::io::stdout().lock(); + stdout.write_all(&captured)?; + stdout.flush()?; + + std::process::exit(crate::exit_code::exit_code_from_status(status)); +} diff --git a/crates/vp_cli_snapshots/src/bin/vpt/main.rs b/crates/vp_cli_snapshots/src/bin/vpt/main.rs index 84a0660285..9502b97b44 100644 --- a/crates/vp_cli_snapshots/src/bin/vpt/main.rs +++ b/crates/vp_cli_snapshots/src/bin/vpt/main.rs @@ -19,6 +19,7 @@ mod exit; mod exit_code; mod exit_on_ctrlc; mod grep_file; +mod head_lines; mod json_edit; mod list_dir; mod mkdir; @@ -64,7 +65,7 @@ fn main() { if args.len() < 2 { eprintln!("Usage: vpt [args...]"); eprintln!( - "Subcommands: backpressure-run (Unix), barrier, check-tty, chmod, cp, exit, exit-on-ctrlc, grep-file, json-edit, list-dir, mkdir, pipe-stdin, print, print-color, print-cwd, print-env, print-file, print-native-path, probe, read-stdin, replace-file-content, rm, stat-file, touch-file, write-file" + "Subcommands: backpressure-run (Unix), barrier, check-tty, chmod, cp, exit, exit-on-ctrlc, grep-file, head-lines, json-edit, list-dir, mkdir, pipe-stdin, print, print-color, print-cwd, print-env, print-file, print-native-path, probe, read-stdin, replace-file-content, rm, stat-file, touch-file, write-file" ); std::process::exit(1); } @@ -85,6 +86,7 @@ fn main() { "exit" => exit::run(&args[2..]), "exit-on-ctrlc" => exit_on_ctrlc::run(), "grep-file" => grep_file::run(&args[2..]), + "head-lines" => head_lines::run(&args[2..]), "json-edit" => json_edit::run(&args[2..]), "list-dir" => list_dir::run(&args[2..]), "mkdir" => mkdir::run(&args[2..]), diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/README.md b/crates/vp_cli_snapshots/tests/cli_snapshots/README.md index 9129433673..605cc66c61 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/README.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/README.md @@ -144,7 +144,9 @@ is identical on every platform: `test -f x && cmd` guards keep their short-circuit), `vpt write-file`, `vpt touch-file`, `vpt replace-file-content`, `vpt list-dir`, `vpt mkdir`, `vpt rm`, `vpt cp`, `vpt chmod`, `vpt grep-file`, `vpt json-edit`, -`vpt pipe-stdin -- `, plus task payloads for `vp run` tests: +`vpt pipe-stdin -- `, +`vpt head-lines -- ` (closes the child's stdout after the selected +lines), plus task payloads for `vp run` tests: `vpt print`, `vpt print-color`, `vpt print-env`, `vpt print-cwd`, `vpt print-native-path` (prints OS-native separators, for redaction self-tests), `vpt check-tty`, `vpt read-stdin`, `vpt exit `, diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_list_remote/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_list_remote/snapshots.toml index ecbef22d4e..9277c0d854 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_list_remote/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_list_remote/snapshots.toml @@ -28,3 +28,12 @@ steps = [ { argv = ["vp", "env", "list", "node"], comment = "Installed Node.js versions retain their interactive formatting", formatted-snapshot = true }, { argv = ["vp", "env", "list", "pnpm"], comment = "Installed package-manager versions use the same interactive formatting", formatted-snapshot = true }, ] + +[[case]] +name = "command_env_list_remote_closed_pipe" +vp = "global" +skip-platforms = ["windows"] +seed-runtime = false +steps = [ + { argv = ["vpt", "head-lines", "1", "--", "vp", "env", "list-remote", "--lts"], tty = false, comment = "Closing stdout after the heading does not make remote version listing fail" }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_list_remote/snapshots/command_env_list_remote_closed_pipe.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_list_remote/snapshots/command_env_list_remote_closed_pipe.md new file mode 100644 index 0000000000..1cced8a2c6 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_list_remote/snapshots/command_env_list_remote_closed_pipe.md @@ -0,0 +1,10 @@ +# command_env_list_remote_closed_pipe + +## `vpt head-lines 1 -- vp env list-remote --lts` + +Closing stdout after the heading does not make remote version listing fail + +``` +Node.js +note: Run `vp env clean` to free disk space from unused managed runtimes and package manager caches. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_version/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_version/snapshots.toml index f2f179124d..d2fc96416f 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_version/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_version/snapshots.toml @@ -5,3 +5,11 @@ skip-platforms = ["windows"] steps = [ { argv = ["vp", "--version"], continue-on-failure = true }, ] + +[[case]] +name = "command_version_closed_pipe" +vp = "global" +skip-platforms = ["windows"] +steps = [ + { argv = ["vpt", "head-lines", "1", "--", "vp", "--version"], tty = false, comment = "Closing stdout after the first line does not make the version command fail" }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_version/snapshots/command_version_closed_pipe.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_version/snapshots/command_version_closed_pipe.md new file mode 100644 index 0000000000..b66f448193 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_version/snapshots/command_version_closed_pipe.md @@ -0,0 +1,9 @@ +# command_version_closed_pipe + +## `vpt head-lines 1 -- vp --version` + +Closing stdout after the first line does not make the version command fail + +``` +vp +``` From 66925fb0af0410bb58ef355c1728e3719b74c45c Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Tue, 22 Sep 2026 22:40:17 +0900 Subject: [PATCH 04/10] fix: improve print_and_flush to handle broken pipes and temporary errors --- crates/vp_shared/src/output.rs | 76 +++++++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 11 deletions(-) diff --git a/crates/vp_shared/src/output.rs b/crates/vp_shared/src/output.rs index 96412b647d..3154f52d5b 100644 --- a/crates/vp_shared/src/output.rs +++ b/crates/vp_shared/src/output.rs @@ -15,21 +15,33 @@ use console::style; /// /// Use this for command output that can be piped to a reader which exits early. pub fn print_and_flush(writer: &mut dyn Write, message: &str) { - writer.write_all(message.as_bytes()).or_else(check_for_writer_error).unwrap(); - writer.flush().or_else(check_for_writer_error).unwrap(); -} + let mut remaining = message.as_bytes(); + while !remaining.is_empty() { + match writer.write(remaining) { + Ok(0) => fail_for_writer_error(io::ErrorKind::WriteZero.into()), + Ok(written) => remaining = &remaining[written..], + Err(error) if error.kind() == io::ErrorKind::BrokenPipe => return, + Err(error) if error.kind() == io::ErrorKind::Interrupted => {} + Err(error) if error.kind() == io::ErrorKind::WouldBlock => std::thread::yield_now(), + Err(error) => fail_for_writer_error(error), + } + } -fn check_for_writer_error(error: io::Error) -> io::Result<()> { - if matches!( - error.kind(), - io::ErrorKind::Interrupted | io::ErrorKind::BrokenPipe | io::ErrorKind::WouldBlock - ) { - Ok(()) - } else { - Err(error) + loop { + match writer.flush() { + Ok(()) => return, + Err(error) if error.kind() == io::ErrorKind::BrokenPipe => return, + Err(error) if error.kind() == io::ErrorKind::Interrupted => {} + Err(error) if error.kind() == io::ErrorKind::WouldBlock => std::thread::yield_now(), + Err(error) => fail_for_writer_error(error), + } } } +fn fail_for_writer_error(error: io::Error) -> ! { + panic!("failed writing command output: {error}"); +} + /// When set, user-facing stdout output (info/pass/note/success/raw) is routed /// to stderr instead. Shim dispatch enables this once at entry: a shim's /// stdout belongs to the wrapped tool and must stay parseable. @@ -140,6 +152,48 @@ pub fn raw_stderr(msg: &str) { mod tests { use super::*; + #[derive(Default)] + struct RetryWriter { + output: Vec, + write_calls: usize, + flush_calls: usize, + } + + impl Write for RetryWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.write_calls += 1; + match self.write_calls { + 1 => { + let written = buf.len().min(2); + self.output.extend_from_slice(&buf[..written]); + Ok(written) + } + 2 => Err(io::ErrorKind::WouldBlock.into()), + _ => { + self.output.extend_from_slice(buf); + Ok(buf.len()) + } + } + } + + fn flush(&mut self) -> io::Result<()> { + self.flush_calls += 1; + match self.flush_calls { + 1 => Err(io::ErrorKind::Interrupted.into()), + 2 => Err(io::ErrorKind::WouldBlock.into()), + _ => Ok(()), + } + } + } + + #[test] + fn print_and_flush_retries_temporary_errors_without_losing_output() { + let mut writer = RetryWriter::default(); + print_and_flush(&mut writer, "output\n"); + assert_eq!(writer.output, b"output\n"); + assert_eq!(writer.flush_calls, 3); + } + #[cfg(unix)] #[test] fn print_and_flush_tolerates_a_closed_pipe() { From 8691214b1655eaabf5e2e61abaa4cd5e10db2f3e Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Wed, 23 Sep 2026 01:07:49 +0900 Subject: [PATCH 05/10] docs: require broken pipe-safe command output --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index c8c8db2d5b..47bbc21c19 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,7 @@ This guide applies to this repository; `CLAUDE.md` points here. For changes to t - Keep public test imports on `vite-plus/test*`, which wraps upstream `vitest` and `@vitest/browser*`. Do not recreate `packages/test` or `@voidzero-dev/vite-plus-test`. - Vite Task crates are git dependencies in `Cargo.toml`; there is no local `crates/vt`. Do not run `cargo test -p vt` here. - Use `vp_shared::VpDirs` for Vite+ directory roots. See `crates/vp_shared/src/dirs.rs` and `crates/vp_shared/src/dirs/resolution.rs`. Call sites must not construct category paths or read `VP_HOME` or `XDG_*` directly. -- Follow [`.clippy.toml`](.clippy.toml) for Rust restrictions and replacements. Use `crates/vp_shared/src/output.rs` for user-facing output and `crates/vp_shared/src/env_config.rs` for test-scoped environment configuration. See `crates/vp_command/src/lib.rs` for `vt_path` usage. +- Follow [`.clippy.toml`](.clippy.toml) for Rust restrictions and replacements. Use `crates/vp_shared/src/output.rs` for user-facing output; command output that can be piped must use `print_and_flush` or another broken-pipe-safe helper instead of `print!` or `println!`. Enable `clippy::print_stdout` in migrated modules so CI prevents direct stdout macros from returning. Use `crates/vp_shared/src/env_config.rs` for test-scoped environment configuration. See `crates/vp_command/src/lib.rs` for `vt_path` usage. - For TypeScript CLI output, use `packages/cli/src/utils/terminal.ts` and match the surrounding command style. - Keep changes scoped to the task and leave unrelated tracked and untracked files alone. Prefer source references over duplicated instructions in this guide. From 32946dc921f8008c045a18c2d6eb7277706c7717 Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Wed, 23 Sep 2026 01:01:33 +0900 Subject: [PATCH 06/10] chore: enforce safe output in migrated commands --- crates/vp_global_cli/src/commands/env/list_remote.rs | 2 ++ crates/vp_global_cli/src/commands/env/mod.rs | 1 + crates/vp_global_cli/src/commands/version.rs | 2 ++ 3 files changed, 5 insertions(+) diff --git a/crates/vp_global_cli/src/commands/env/list_remote.rs b/crates/vp_global_cli/src/commands/env/list_remote.rs index 7c07d236bd..bf86eae4b9 100644 --- a/crates/vp_global_cli/src/commands/env/list_remote.rs +++ b/crates/vp_global_cli/src/commands/env/list_remote.rs @@ -1,3 +1,5 @@ +#![deny(clippy::print_stdout)] + use std::{collections::BTreeMap, io, process::ExitStatus}; use console::style; diff --git a/crates/vp_global_cli/src/commands/env/mod.rs b/crates/vp_global_cli/src/commands/env/mod.rs index d2206e172a..f270482900 100644 --- a/crates/vp_global_cli/src/commands/env/mod.rs +++ b/crates/vp_global_cli/src/commands/env/mod.rs @@ -40,6 +40,7 @@ fn print_env_header() { vp_shared::header::print_header(); } +#[deny(clippy::print_stdout)] fn print_env_clean_tip() { vp_shared::output::print_and_flush(&mut io::stdout().lock(), "\n"); vp_shared::output::note( diff --git a/crates/vp_global_cli/src/commands/version.rs b/crates/vp_global_cli/src/commands/version.rs index 1364cbec1b..c35976392e 100644 --- a/crates/vp_global_cli/src/commands/version.rs +++ b/crates/vp_global_cli/src/commands/version.rs @@ -1,5 +1,7 @@ //! Version command. +#![deny(clippy::print_stdout)] + use std::{ collections::BTreeMap, fs, io, From 3c72c9e45a983cd6a82b9a6eab2de6b8bd730db2 Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Wed, 23 Sep 2026 02:54:02 +0900 Subject: [PATCH 07/10] refactor: make shared output broken-pipe safe --- crates/vp_shared/src/header.rs | 13 ++++-- crates/vp_shared/src/lib.rs | 7 +-- crates/vp_shared/src/output.rs | 80 ++++++++++++++++++++++++---------- 3 files changed, 68 insertions(+), 32 deletions(-) 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..61dcfb269c 100644 --- a/crates/vp_shared/src/lib.rs +++ b/crates/vp_shared/src/lib.rs @@ -1,11 +1,6 @@ //! Shared utilities for vite-plus crates -#![allow( - clippy::allow_attributes, - clippy::disallowed_macros, - clippy::disallowed_types, - clippy::print_stdout -)] +#![allow(clippy::allow_attributes, clippy::disallowed_macros, 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)] From c5040a74b7d958300174950133f8921ab5158238 Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Wed, 23 Sep 2026 02:55:02 +0900 Subject: [PATCH 08/10] refactor: migrate global CLI output helpers --- crates/vp_global_cli/src/cli.rs | 2 +- .../vp_global_cli/src/commands/env/current.rs | 15 ++- .../vp_global_cli/src/commands/env/default.rs | 45 +++++-- .../vp_global_cli/src/commands/env/doctor.rs | 126 ++++++++++++------ .../src/commands/env/lifecycle.rs | 10 +- crates/vp_global_cli/src/commands/env/list.rs | 17 ++- .../src/commands/env/list_remote.rs | 2 - crates/vp_global_cli/src/commands/env/mod.rs | 7 +- crates/vp_global_cli/src/commands/env/off.rs | 15 ++- crates/vp_global_cli/src/commands/env/on.rs | 15 ++- crates/vp_global_cli/src/commands/env/pin.rs | 112 ++++++++++------ crates/vp_global_cli/src/commands/env/use.rs | 24 +++- .../vp_global_cli/src/commands/env/which.rs | 66 +++++---- .../src/commands/global/outdated.rs | 54 +++++--- .../src/commands/global/packages.rs | 37 +++-- .../vp_global_cli/src/commands/upgrade/mod.rs | 39 ++++-- crates/vp_global_cli/src/commands/version.rs | 2 - crates/vp_global_cli/src/commands/vpx.rs | 2 +- crates/vp_global_cli/src/main.rs | 46 +++++-- crates/vp_global_cli/src/self_setup.rs | 9 +- 20 files changed, 429 insertions(+), 216 deletions(-) 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/list_remote.rs b/crates/vp_global_cli/src/commands/env/list_remote.rs index bf86eae4b9..7c07d236bd 100644 --- a/crates/vp_global_cli/src/commands/env/list_remote.rs +++ b/crates/vp_global_cli/src/commands/env/list_remote.rs @@ -1,5 +1,3 @@ -#![deny(clippy::print_stdout)] - use std::{collections::BTreeMap, io, process::ExitStatus}; use console::style; diff --git a/crates/vp_global_cli/src/commands/env/mod.rs b/crates/vp_global_cli/src/commands/env/mod.rs index f270482900..cfe3f3d741 100644 --- a/crates/vp_global_cli/src/commands/env/mod.rs +++ b/crates/vp_global_cli/src/commands/env/mod.rs @@ -40,7 +40,6 @@ fn print_env_header() { vp_shared::header::print_header(); } -#[deny(clippy::print_stdout)] fn print_env_clean_tip() { vp_shared::output::print_and_flush(&mut io::stdout().lock(), "\n"); vp_shared::output::note( @@ -213,8 +212,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/version.rs b/crates/vp_global_cli/src/commands/version.rs index c35976392e..1364cbec1b 100644 --- a/crates/vp_global_cli/src/commands/version.rs +++ b/crates/vp_global_cli/src/commands/version.rs @@ -1,7 +1,5 @@ //! Version command. -#![deny(clippy::print_stdout)] - use std::{ collections::BTreeMap, fs, io, 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) + )); } } } From 91b98684ca71f5cd62938b7ec96ae55dec8bb913 Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Wed, 23 Sep 2026 19:05:14 +0900 Subject: [PATCH 09/10] style: format clippy allow attributes for better readability --- crates/vp_shared/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/vp_shared/src/lib.rs b/crates/vp_shared/src/lib.rs index 61dcfb269c..4d47cb335e 100644 --- a/crates/vp_shared/src/lib.rs +++ b/crates/vp_shared/src/lib.rs @@ -1,6 +1,10 @@ //! Shared utilities for vite-plus crates -#![allow(clippy::allow_attributes, clippy::disallowed_macros, clippy::disallowed_types)] +#![allow( + clippy::allow_attributes, + clippy::disallowed_macros, + clippy::disallowed_types +)] mod dirs; mod documentation; From 9e0e1f57e0e45f32e1c958ebacd1fdf3897d79a3 Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Wed, 23 Sep 2026 19:09:50 +0900 Subject: [PATCH 10/10] style: simplify clippy allow attributes for improved readability --- crates/vp_shared/src/lib.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/vp_shared/src/lib.rs b/crates/vp_shared/src/lib.rs index 4d47cb335e..5afb874d9a 100644 --- a/crates/vp_shared/src/lib.rs +++ b/crates/vp_shared/src/lib.rs @@ -1,10 +1,8 @@ //! Shared utilities for vite-plus crates -#![allow( - clippy::allow_attributes, - clippy::disallowed_macros, - clippy::disallowed_types -)] +#![allow(clippy::allow_attributes)] +#![allow(clippy::disallowed_macros)] +#![allow(clippy::disallowed_types)] mod dirs; mod documentation;