@@ -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) ]
199277async 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) ]
450534fn print_error ( msg : & str ) {
451535 eprint ! ( "{}" , "error: " . red( ) ) ;
0 commit comments