Skip to content

Commit 733b973

Browse files
authored
refactor: make global CLI stdout broken-pipe safe (#2793)
This PR follows up on #2785. I updated the remaining print! and println! calls in vp_global_cli and vp_shared so they now use our shared output helper. I also removed the crate-wide attribute that allowed clippy::print_stdout across the board. Now, if an AI agent reading AGENTS.md accidentally adds a direct print call, CI will catch it right away. The main benefit here is that piping commands into tools like head, which close stdout early, will no longer cause a panic and can exit cleanly instead. This change does not affect the actual content or formatting of the output text.
1 parent 04aa8dd commit 733b973

21 files changed

Lines changed: 499 additions & 243 deletions

File tree

‎crates/vp_global_cli/src/cli.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1092,7 +1092,7 @@ pub async fn run_command_with_options(
10921092
} else {
10931093
command_with_help_with_options(render_options).print_help().ok();
10941094
}
1095-
println!();
1095+
vp_shared::output::print_stdout_line(format_args!(""));
10961096
// Return a successful exit status since help was requested implicitly
10971097
return Ok(std::process::ExitStatus::default());
10981098
};

‎crates/vp_global_cli/src/commands/env/current.rs‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,15 @@ struct PackageManagerInfo {
4747
}
4848

4949
fn print_rows(title: &str, rows: &[(String, String)]) {
50-
println!("{}", help::render_heading(title));
50+
vp_shared::output::print_stdout_line(format_args!("{}", help::render_heading(title)));
5151
let label_width = rows.iter().map(|(label, _)| label.chars().count()).max().unwrap_or(0);
5252
for (label, value) in rows {
5353
let padding = " ".repeat(label_width.saturating_sub(label.chars().count()));
54-
println!(" {}{} {value}", help::accent(label), padding);
54+
vp_shared::output::print_stdout_line(format_args!(
55+
" {}{} {value}",
56+
help::accent(label),
57+
padding
58+
));
5559
}
5660
}
5761

@@ -108,7 +112,10 @@ pub async fn execute(
108112
};
109113

110114
if json {
111-
println!("{}", serde_json::to_string_pretty(&CurrentEnvInfo { node, package_manager })?);
115+
vp_shared::output::print_stdout_line(format_args!(
116+
"{}",
117+
serde_json::to_string_pretty(&CurrentEnvInfo { node, package_manager })?
118+
));
112119
return Ok(ExitStatus::default());
113120
}
114121

@@ -126,7 +133,7 @@ pub async fn execute(
126133
}
127134
if let Some(package_manager) = package_manager {
128135
if scope.includes_node() {
129-
println!();
136+
vp_shared::output::print_stdout_line(format_args!(""));
130137
}
131138
let mut rows = vec![
132139
("Name".into(), package_manager.name),

‎crates/vp_global_cli/src/commands/env/default.rs‎

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ pub async fn execute(values: Vec<String>, unset: bool) -> Result<ExitStatus, Err
2929
}
3030
save_config(&config).await?;
3131
crate::shim::invalidate_cache();
32-
println!("Cleared selected environment defaults.");
32+
vp_shared::output::print_stdout_line(format_args!(
33+
"Cleared selected environment defaults."
34+
));
3335
return Ok(ExitStatus::default());
3436
}
3537

@@ -67,7 +69,7 @@ pub async fn execute(values: Vec<String>, unset: bool) -> Result<ExitStatus, Err
6769
save_config(&config).await?;
6870
crate::shim::invalidate_cache();
6971
for update in updates {
70-
println!("\u{2713} {update}");
72+
vp_shared::output::print_stdout_line(format_args!("\u{2713} {update}"));
7173
}
7274
Ok(ExitStatus::default())
7375
}
@@ -79,27 +81,35 @@ async fn show_default(scope: EnvScope) -> Result<ExitStatus, Error> {
7981
match config.default_node_version.as_deref() {
8082
Some(version) => {
8183
has_configured_default = true;
82-
println!("Default Node.js version: {version}");
84+
vp_shared::output::print_stdout_line(format_args!(
85+
"Default Node.js version: {version}"
86+
));
8387
if matches!(version, "lts" | "latest") {
8488
let provider = vp_js_runtime::NodeProvider::new();
8589
if let Ok(resolved) =
8690
super::config::resolve_version_alias(version, &provider).await
8791
{
88-
println!(" Currently resolves to: {resolved}");
92+
vp_shared::output::print_stdout_line(format_args!(
93+
" Currently resolves to: {resolved}"
94+
));
8995
}
9096
}
9197
}
9298
None => {
9399
let provider = vp_js_runtime::NodeProvider::new();
94100
match provider.resolve_latest_version().await {
95101
Ok(version) => {
96-
println!(
102+
vp_shared::output::print_stdout_line(format_args!(
97103
"No default Node.js version configured. Using latest LTS ({version})."
98-
);
104+
));
99105
}
100-
Err(_) => println!("No default Node.js version configured."),
106+
Err(_) => vp_shared::output::print_stdout_line(format_args!(
107+
"No default Node.js version configured."
108+
)),
101109
}
102-
println!(" Run 'vp env default <version>' to set a default.");
110+
vp_shared::output::print_stdout_line(format_args!(
111+
" Run 'vp env default <version>' to set a default."
112+
));
103113
}
104114
}
105115
}
@@ -115,20 +125,27 @@ async fn show_default(scope: EnvScope) -> Result<ExitStatus, Error> {
115125
.collect::<Vec<_>>();
116126
if configured.is_empty() {
117127
match scope {
118-
EnvScope::PackageManager(kind) => {
119-
println!("Default {kind} version: not configured")
120-
}
121-
_ => println!("Package manager defaults: not configured"),
128+
EnvScope::PackageManager(kind) => vp_shared::output::print_stdout_line(
129+
format_args!("Default {kind} version: not configured"),
130+
),
131+
_ => vp_shared::output::print_stdout_line(format_args!(
132+
"Package manager defaults: not configured"
133+
)),
122134
}
123135
} else {
124136
for (package_manager, version) in configured {
125137
has_configured_default = true;
126-
println!("Default {package_manager} version: {version}");
138+
vp_shared::output::print_stdout_line(format_args!(
139+
"Default {package_manager} version: {version}"
140+
));
127141
}
128142
}
129143
}
130144
if has_configured_default {
131-
println!(" Set via: {}", get_config_path()?.as_path().display());
145+
vp_shared::output::print_stdout_line(format_args!(
146+
" Set via: {}",
147+
get_config_path()?.as_path().display()
148+
));
132149
}
133150
Ok(ExitStatus::default())
134151
}

‎crates/vp_global_cli/src/commands/env/doctor.rs‎

Lines changed: 85 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ const KEY_WIDTH: usize = 18;
4343

4444
/// Print a section header (bold, with blank line before).
4545
fn print_section(name: &str) {
46-
println!();
47-
println!("{}", style(&name).bold());
46+
vp_shared::output::print_stdout_line(format_args!(""));
47+
vp_shared::output::print_stdout_line(format_args!("{}", style(&name).bold()));
4848
}
4949

5050
/// Print an aligned key-value line with a status indicator.
@@ -53,11 +53,11 @@ fn print_section(name: &str) {
5353
/// Use `" "` for informational lines with no status.
5454
fn print_check(status: &str, key: &str, value: &str) {
5555
if status.trim().is_empty() {
56-
println!(" {key:<KEY_WIDTH$}{value}");
56+
vp_shared::output::print_stdout_line(format_args!(" {key:<KEY_WIDTH$}{value}"));
5757
} else if key.trim().is_empty() {
58-
println!(" {status} {value}");
58+
vp_shared::output::print_stdout_line(format_args!(" {status} {value}"));
5959
} else {
60-
println!(" {status} {key:<KEY_WIDTH$}{value}");
60+
vp_shared::output::print_stdout_line(format_args!(" {status} {key:<KEY_WIDTH$}{value}"));
6161
}
6262
}
6363

@@ -71,7 +71,10 @@ fn print_package_manager_mode(key: &str, mode: ShimMode) {
7171

7272
/// Print a continuation/hint line (dimmed).
7373
fn print_hint(text: &str) {
74-
println!(" {}", style(format!("note: {text}")).dim());
74+
vp_shared::output::print_stdout_line(format_args!(
75+
" {}",
76+
style(format!("note: {text}")).dim()
77+
));
7578
}
7679

7780
/// Abbreviate home directory to `~` for display.
@@ -90,7 +93,7 @@ pub async fn execute(cwd: AbsolutePathBuf, scope: Option<String>) -> Result<Exit
9093
let mut has_errors = false;
9194

9295
// Section: Installation
93-
println!("{}", style("Installation").bold());
96+
vp_shared::output::print_stdout_line(format_args!("{}", style("Installation").bold()));
9497
if crate::homebrew::owns_current_exe() {
9598
print_check(" ", "CLI source", "Homebrew");
9699
if let Ok(binary) = std::env::current_exe().and_then(std::fs::canonicalize) {
@@ -146,17 +149,20 @@ pub async fn execute(cwd: AbsolutePathBuf, scope: Option<String>) -> Result<Exit
146149
}
147150

148151
// Summary
149-
println!();
152+
vp_shared::output::print_stdout_line(format_args!(""));
150153
if has_errors {
151-
println!(
154+
vp_shared::output::print_stdout_line(format_args!(
152155
"{}",
153156
style("\u{2717} Some issues found. Run the suggested commands to fix them.")
154157
.red()
155158
.bold()
156-
);
159+
));
157160
Ok(super::exit_status(1))
158161
} else {
159-
println!("{}", style("\u{2713} All checks passed").green().bold());
162+
vp_shared::output::print_stdout_line(format_args!(
163+
"{}",
164+
style("\u{2713} All checks passed").green().bold()
165+
));
160166
Ok(ExitStatus::default())
161167
}
162168
}
@@ -559,7 +565,7 @@ async fn check_path(scope: EnvScope) -> bool {
559565
&style("not in PATH").red().to_string(),
560566
);
561567
print_hint(&format!("Expected: {bin_display}"));
562-
println!();
568+
vp_shared::output::print_stdout_line(format_args!(""));
563569
print_path_fix(&vp_shared::EnvConfig::get().dirs.config);
564570
return false;
565571
}
@@ -630,28 +636,48 @@ fn print_path_fix(env_dir: &vt_path::AbsolutePath) {
630636
env_path
631637
};
632638

633-
println!(" {}", style("Add to your shell profile (~/.zshrc, ~/.bashrc, etc.):").dim());
634-
println!();
635-
println!(" . \"{env_path}/env\"");
636-
println!();
637-
println!(" {}", style("For fish shell, add to ~/.config/fish/config.fish:").dim());
638-
println!();
639-
println!(" source \"{env_path}/env.fish\"");
640-
println!();
641-
println!(" {}", style("For Nushell, add to ~/.config/nushell/config.nu:").dim());
642-
println!();
643-
println!(" source '{env_path}/env.nu'");
644-
println!();
645-
println!(" {}", style("Then restart your terminal.").dim());
639+
vp_shared::output::print_stdout_line(format_args!(
640+
" {}",
641+
style("Add to your shell profile (~/.zshrc, ~/.bashrc, etc.):").dim()
642+
));
643+
vp_shared::output::print_stdout_line(format_args!(""));
644+
vp_shared::output::print_stdout_line(format_args!(" . \"{env_path}/env\""));
645+
vp_shared::output::print_stdout_line(format_args!(""));
646+
vp_shared::output::print_stdout_line(format_args!(
647+
" {}",
648+
style("For fish shell, add to ~/.config/fish/config.fish:").dim()
649+
));
650+
vp_shared::output::print_stdout_line(format_args!(""));
651+
vp_shared::output::print_stdout_line(format_args!(" source \"{env_path}/env.fish\""));
652+
vp_shared::output::print_stdout_line(format_args!(""));
653+
vp_shared::output::print_stdout_line(format_args!(
654+
" {}",
655+
style("For Nushell, add to ~/.config/nushell/config.nu:").dim()
656+
));
657+
vp_shared::output::print_stdout_line(format_args!(""));
658+
vp_shared::output::print_stdout_line(format_args!(" source '{env_path}/env.nu'"));
659+
vp_shared::output::print_stdout_line(format_args!(""));
660+
vp_shared::output::print_stdout_line(format_args!(
661+
" {}",
662+
style("Then restart your terminal.").dim()
663+
));
646664
}
647665

648666
#[cfg(windows)]
649667
{
650668
let _ = env_dir;
651-
println!(" {}", style("Add the bin directory to your PATH via:").dim());
652-
println!(" System Properties -> Environment Variables -> Path");
653-
println!();
654-
println!(" {}", style("Then restart your terminal.").dim());
669+
vp_shared::output::print_stdout_line(format_args!(
670+
" {}",
671+
style("Add the bin directory to your PATH via:").dim()
672+
));
673+
vp_shared::output::print_stdout_line(format_args!(
674+
" System Properties -> Environment Variables -> Path"
675+
));
676+
vp_shared::output::print_stdout_line(format_args!(""));
677+
vp_shared::output::print_stdout_line(format_args!(
678+
" {}",
679+
style("Then restart your terminal.").dim()
680+
));
655681
}
656682
}
657683

@@ -705,30 +731,48 @@ fn print_ide_setup_guidance(env_dir: &vt_path::AbsolutePath) {
705731
"",
706732
&style("GUI applications may not see shell PATH changes.").yellow().to_string(),
707733
);
708-
println!();
734+
vp_shared::output::print_stdout_line(format_args!(""));
709735

710736
#[cfg(target_os = "macos")]
711737
{
712-
println!(" {}", style("macOS:").dim());
713-
println!(" {}", style("Add to ~/.zshenv or ~/.profile:").dim());
714-
println!(" . \"{env_path}/env\"");
715-
println!(" {}", style("Then restart your IDE to apply changes.").dim());
738+
vp_shared::output::print_stdout_line(format_args!(" {}", style("macOS:").dim()));
739+
vp_shared::output::print_stdout_line(format_args!(
740+
" {}",
741+
style("Add to ~/.zshenv or ~/.profile:").dim()
742+
));
743+
vp_shared::output::print_stdout_line(format_args!(" . \"{env_path}/env\""));
744+
vp_shared::output::print_stdout_line(format_args!(
745+
" {}",
746+
style("Then restart your IDE to apply changes.").dim()
747+
));
716748
}
717749

718750
#[cfg(target_os = "linux")]
719751
{
720-
println!(" {}", style("Linux:").dim());
721-
println!(" {}", style("Add to ~/.profile:").dim());
722-
println!(" . \"{env_path}/env\"");
723-
println!(" {}", style("Then log out and log back in for changes to take effect.").dim());
752+
vp_shared::output::print_stdout_line(format_args!(" {}", style("Linux:").dim()));
753+
vp_shared::output::print_stdout_line(format_args!(
754+
" {}",
755+
style("Add to ~/.profile:").dim()
756+
));
757+
vp_shared::output::print_stdout_line(format_args!(" . \"{env_path}/env\""));
758+
vp_shared::output::print_stdout_line(format_args!(
759+
" {}",
760+
style("Then log out and log back in for changes to take effect.").dim()
761+
));
724762
}
725763

726764
// Fallback for other Unix platforms
727765
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
728766
{
729-
println!(" {}", style("Add to your shell profile:").dim());
730-
println!(" . \"{env_path}/env\"");
731-
println!(" {}", style("Then restart your IDE to apply changes.").dim());
767+
vp_shared::output::print_stdout_line(format_args!(
768+
" {}",
769+
style("Add to your shell profile:").dim()
770+
));
771+
vp_shared::output::print_stdout_line(format_args!(" . \"{env_path}/env\""));
772+
vp_shared::output::print_stdout_line(format_args!(
773+
" {}",
774+
style("Then restart your IDE to apply changes.").dim()
775+
));
732776
}
733777
}
734778

‎crates/vp_global_cli/src/commands/env/lifecycle.rs‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ pub(crate) async fn install(
5252
}
5353
};
5454
if let Some((version, from_session_override)) = resolved {
55-
println!("Installing Node.js v{version}...");
55+
vp_shared::output::print_stdout_line(format_args!("Installing Node.js v{version}..."));
5656
vp_js_runtime::download_runtime(vp_js_runtime::JsRuntimeType::Node, &version).await?;
57-
println!("Installed Node.js v{version}");
57+
vp_shared::output::print_stdout_line(format_args!("Installed Node.js v{version}"));
5858
if from_session_override {
5959
eprintln!("Note: Installed from session override.");
6060
eprintln!("Run `vp env use --unset` to revert to project version resolution.");
@@ -83,9 +83,9 @@ pub(crate) async fn install(
8383
})
8484
};
8585
if let Some((kind, version, hash)) = requested {
86-
println!("Installing {kind} v{version}...");
86+
vp_shared::output::print_stdout_line(format_args!("Installing {kind} v{version}..."));
8787
download_package_manager(kind, &version, hash.as_deref()).await?;
88-
println!("Installed {kind} v{version}");
88+
vp_shared::output::print_stdout_line(format_args!("Installed {kind} v{version}"));
8989
}
9090
}
9191

@@ -133,7 +133,7 @@ pub(crate) async fn uninstall(specs: Vec<String>) -> Result<ExitStatus, Error> {
133133
}
134134
for (label, target) in targets {
135135
tokio::fs::remove_dir_all(target.as_path()).await?;
136-
println!("Uninstalled {label}");
136+
vp_shared::output::print_stdout_line(format_args!("Uninstalled {label}"));
137137
}
138138
Ok(ExitStatus::default())
139139
}

0 commit comments

Comments
 (0)