Skip to content

Commit e90116f

Browse files
committed
fix(installer): address Codex review findings
Fix all high/P1/P2 issues from both adversarial and standard reviews: 1. Node.js manager auto-detect: port the full auto-detect logic from install.ps1/install.sh instead of unconditionally enabling shims. Checks VP_NODE_MANAGER env, existing shims, CI/devcontainer, system node availability — and prompts interactively when system node exists (matching install.ps1 behavior). Silent mode (-y) skips the prompt and does not enable shims when system node is present. 2. Same-version repair: when the target version is already installed, skip download/extract/deps but still run all post-activation setup (shims, Node.js manager, PATH, env files). This allows rerunning the installer to repair a broken installation. 3. Rollback protection: include the previous version in protected_versions during cleanup, matching the vp upgrade implementation. Prevents cleanup from deleting the rollback target. 4. Post-activation best-effort: setup_bin_shims, refresh_shims, and modify_path are now wrapped in if-let-Err with warnings instead of propagating errors. After activation (current junction swap), the core install has succeeded — configuration failures should not cause exit code 1.
1 parent 02c8389 commit e90116f

3 files changed

Lines changed: 139 additions & 53 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/vite_installer/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ vite_install = { workspace = true }
2121
vite_path = { workspace = true }
2222
vite_setup = { workspace = true }
2323
vite_shared = { workspace = true }
24+
which = { workspace = true }
2425

2526
[target.'cfg(windows)'.dependencies]
2627
winreg = { workspace = true }

crates/vite_installer/src/main.rs

Lines changed: 137 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -108,92 +108,170 @@ async fn do_install(
108108
tokio::fs::create_dir_all(install_dir).await?;
109109

110110
let current_version = install::read_current_version(install_dir).await;
111-
if let Some(ref current) = current_version {
112-
if current == &resolved.version {
111+
let same_version = current_version.as_deref() == Some(resolved.version.as_str());
112+
113+
if same_version {
114+
if !opts.quiet {
115+
print_info(&format!(
116+
"version {} already installed, verifying setup...",
117+
resolved.version
118+
));
119+
}
120+
} else {
121+
if let Some(ref current) = current_version {
113122
if !opts.quiet {
114-
println!("\n{} Already installed ({})", "\u{2714}".green(), resolved.version);
123+
print_info(&format!("upgrading from {current} to {}", resolved.version));
115124
}
116-
return Ok(());
117125
}
126+
118127
if !opts.quiet {
119-
print_info(&format!("upgrading from {current} to {}", resolved.version));
128+
print_info(&format!(
129+
"downloading vite-plus@{} for {}...",
130+
resolved.version, platform_suffix
131+
));
120132
}
121-
}
133+
let client = HttpClient::new();
134+
let platform_data =
135+
download_with_progress(&client, &resolved.platform_tarball_url, opts.quiet).await?;
122136

123-
if !opts.quiet {
124-
print_info(&format!(
125-
"downloading vite-plus@{} for {}...",
126-
resolved.version, platform_suffix
127-
));
128-
}
129-
let client = HttpClient::new();
130-
let platform_data =
131-
download_with_progress(&client, &resolved.platform_tarball_url, opts.quiet).await?;
137+
if !opts.quiet {
138+
print_info("verifying integrity...");
139+
}
140+
integrity::verify_integrity(&platform_data, &resolved.platform_integrity)?;
132141

133-
if !opts.quiet {
134-
print_info("verifying integrity...");
135-
}
136-
integrity::verify_integrity(&platform_data, &resolved.platform_integrity)?;
142+
let version_dir = install_dir.join(&resolved.version);
143+
tokio::fs::create_dir_all(&version_dir).await?;
144+
145+
if !opts.quiet {
146+
print_info("extracting binary...");
147+
}
148+
install::extract_platform_package(&platform_data, &version_dir).await?;
137149

138-
let version_dir = install_dir.join(&resolved.version);
139-
tokio::fs::create_dir_all(&version_dir).await?;
150+
let binary_path = version_dir.join("bin").join(VP_BINARY_NAME);
151+
if !tokio::fs::try_exists(&binary_path).await.unwrap_or(false) {
152+
return Err("Binary not found after extraction. The download may be corrupted.".into());
153+
}
140154

141-
if !opts.quiet {
142-
print_info("extracting binary...");
143-
}
144-
install::extract_platform_package(&platform_data, &version_dir).await?;
155+
install::generate_wrapper_package_json(&version_dir, &resolved.version).await?;
156+
install::write_release_age_overrides(&version_dir).await?;
145157

146-
let binary_path = version_dir.join("bin").join(VP_BINARY_NAME);
147-
if !tokio::fs::try_exists(&binary_path).await.unwrap_or(false) {
148-
return Err("Binary not found after extraction. The download may be corrupted.".into());
149-
}
158+
if !opts.quiet {
159+
print_info("installing dependencies (this may take a moment)...");
160+
}
161+
install::install_production_deps(&version_dir, opts.registry.as_deref()).await?;
150162

151-
install::generate_wrapper_package_json(&version_dir, &resolved.version).await?;
152-
install::write_release_age_overrides(&version_dir).await?;
163+
let previous_version = if current_version.is_some() {
164+
install::save_previous_version(install_dir).await?
165+
} else {
166+
None
167+
};
168+
install::swap_current_link(install_dir, &resolved.version).await?;
153169

154-
if !opts.quiet {
155-
print_info("installing dependencies (this may take a moment)...");
170+
// Cleanup with both new and previous versions protected (matches vp upgrade)
171+
let mut protected = vec![resolved.version.as_str()];
172+
if let Some(ref prev) = previous_version {
173+
protected.push(prev.as_str());
174+
}
175+
if let Err(e) =
176+
install::cleanup_old_versions(install_dir, vite_setup::MAX_VERSIONS_KEEP, &protected)
177+
.await
178+
{
179+
tracing::warn!("Old version cleanup failed (non-fatal): {e}");
180+
}
156181
}
157-
install::install_production_deps(&version_dir, opts.registry.as_deref()).await?;
158182

159-
if current_version.is_some() {
160-
install::save_previous_version(install_dir).await?;
161-
}
162-
install::swap_current_link(install_dir, &resolved.version).await?;
183+
// --- Post-activation setup (always runs, even for same-version repair) ---
184+
// All steps below are best-effort after activation: the core install succeeded
185+
// once `current` points at the right version.
163186

164187
if !opts.quiet {
165188
print_info("setting up shims...");
166189
}
167-
setup_bin_shims(install_dir).await?;
190+
if let Err(e) = setup_bin_shims(install_dir).await {
191+
print_warn(&format!("Shim setup failed (non-fatal): {e}"));
192+
}
168193

169-
if !opts.no_node_manager {
194+
// Node.js manager: match install.ps1/install.sh auto-detect logic
195+
let enable_node_manager = should_enable_node_manager(opts, install_dir);
196+
if enable_node_manager {
170197
if !opts.quiet {
171198
print_info("setting up Node.js version manager...");
172199
}
173-
install::refresh_shims(install_dir).await?;
200+
if let Err(e) = install::refresh_shims(install_dir).await {
201+
print_warn(&format!("Node.js manager setup failed (non-fatal): {e}"));
202+
}
174203
} else {
175-
// When skipping Node.js manager, still create shell env files
204+
// Still create shell env files even without Node.js manager
176205
create_env_files(install_dir).await;
177206
}
178207

179-
if let Err(e) = install::cleanup_old_versions(
180-
install_dir,
181-
vite_setup::MAX_VERSIONS_KEEP,
182-
&[&resolved.version],
183-
)
184-
.await
185-
{
186-
tracing::warn!("Old version cleanup failed (non-fatal): {e}");
187-
}
188-
189208
if !opts.no_modify_path {
190209
let bin_dir_str = install_dir.join("bin").as_path().to_string_lossy().to_string();
191-
modify_path(&bin_dir_str, opts.quiet)?;
210+
if let Err(e) = modify_path(&bin_dir_str, opts.quiet) {
211+
print_warn(&format!("PATH modification failed (non-fatal): {e}"));
212+
}
192213
}
193214

194215
Ok(())
195216
}
196217

218+
/// Determine whether to enable the Node.js version manager (node/npm/npx shims).
219+
///
220+
/// Matches the auto-detect logic from install.ps1/install.sh:
221+
/// 1. VP_NODE_MANAGER=yes → enable; VP_NODE_MANAGER=no or --no-node-manager → disable
222+
/// 2. Already managing Node (bin/node.exe exists) → enable (refresh)
223+
/// 3. CI / Codespaces / DevContainer / DevPod → enable
224+
/// 4. No system `node` found → enable
225+
/// 5. Interactive mode with system node → prompt the user
226+
/// 6. Silent mode with system node → disable (don't silently take over)
227+
#[allow(clippy::print_stdout)]
228+
fn should_enable_node_manager(opts: &cli::Options, install_dir: &vite_path::AbsolutePath) -> bool {
229+
if opts.no_node_manager {
230+
return false;
231+
}
232+
233+
if std::env::var("VP_NODE_MANAGER").ok().is_some_and(|v| v.eq_ignore_ascii_case("yes")) {
234+
return true;
235+
}
236+
237+
// Already managing Node (shims exist from a previous install)
238+
let node_shim = install_dir.join("bin").join(if cfg!(windows) { "node.exe" } else { "node" });
239+
if node_shim.as_path().exists() {
240+
return true;
241+
}
242+
243+
// Auto-enable on CI / devcontainer environments
244+
if std::env::var_os("CI").is_some()
245+
|| std::env::var_os("CODESPACES").is_some()
246+
|| std::env::var_os("REMOTE_CONTAINERS").is_some()
247+
|| std::env::var_os("DEVPOD").is_some()
248+
{
249+
return true;
250+
}
251+
252+
// Auto-enable if no system node available
253+
if which::which("node").is_err() {
254+
return true;
255+
}
256+
257+
// System node exists — prompt in interactive mode, skip in silent mode
258+
if opts.yes {
259+
return false;
260+
}
261+
262+
println!();
263+
println!(" Would you like Vite+ to manage your Node.js versions?");
264+
println!(
265+
" It adds {}, {}, and {} shims to ~/.vite-plus/bin/ and automatically uses the right version.",
266+
"node".cyan(),
267+
"npm".cyan(),
268+
"npx".cyan()
269+
);
270+
println!(" Opt out anytime with {}.", "vp env off".cyan());
271+
let answer = read_input(" Press Enter to accept (Y/n): ");
272+
answer.is_empty() || answer.eq_ignore_ascii_case("y")
273+
}
274+
197275
/// Windows locks running `.exe` files — rename the old one out of the way before copying.
198276
#[cfg(windows)]
199277
async fn replace_windows_exe(
@@ -446,6 +524,12 @@ fn print_info(msg: &str) {
446524
eprintln!("{msg}");
447525
}
448526

527+
#[allow(clippy::print_stderr)]
528+
fn print_warn(msg: &str) {
529+
eprint!("{}", "warn: ".yellow());
530+
eprintln!("{msg}");
531+
}
532+
449533
#[allow(clippy::print_stderr)]
450534
fn print_error(msg: &str) {
451535
eprint!("{}", "error: ".red());

0 commit comments

Comments
 (0)