@@ -50,6 +50,10 @@ final class EvoBootstrapper
5050 return 0 ;
5151 }
5252
53+ if ($ cmd === 'install ' ) {
54+ $ this ->maybePromptUpdateBeforeInstall ();
55+ }
56+
5357 if (!is_file ($ this ->binaryPath )) {
5458 $ this ->installBinary (false );
5559 }
@@ -267,6 +271,54 @@ final class EvoBootstrapper
267271 $ this ->installBinary (true );
268272 }
269273
274+ private function maybePromptUpdateBeforeInstall (): void
275+ {
276+ if (!$ this ->isInteractiveSession ()) {
277+ return ;
278+ }
279+
280+ if ($ this ->hasArg ('--no-interaction ' ) || $ this ->hasArg ('-n ' )) {
281+ return ;
282+ }
283+ if ($ this ->hasArg ('--quiet ' ) || $ this ->hasArg ('-q ' ) || $ this ->hasArg ('--silent ' )) {
284+ return ;
285+ }
286+ if ($ this ->isCiEnvironment ()) {
287+ return ;
288+ }
289+
290+ $ current = $ this ->getInstalledVersion ();
291+ if (!is_string ($ current ) || trim ($ current ) === '' ) {
292+ return ;
293+ }
294+
295+ $ latest = null ;
296+ try {
297+ [$ tag ] = $ this ->getLatestReleaseInfo ();
298+ $ latest = $ tag ;
299+ } catch (RuntimeException $ e ) {
300+ $ latest = $ this ->resolveLatestReleaseTagFallback ();
301+ }
302+
303+ if (!is_string ($ latest ) || trim ($ latest ) === '' ) {
304+ return ;
305+ }
306+
307+ if (!$ this ->isNewerVersion ($ current , $ latest )) {
308+ return ;
309+ }
310+
311+ $ this ->out ("Update available: {$ current } -> {$ latest }" );
312+ if (!$ this ->promptYesNo ('Update now? [Y/n] ' , true )) {
313+ $ this ->out ('Continuing without update. ' );
314+ return ;
315+ }
316+
317+ $ this ->out ('Updating installer (Composer + binary)... ' );
318+ $ this ->updatePhpInstallerPackageGlobal ();
319+ $ this ->installBinary (true );
320+ }
321+
270322 private function updatePhpInstallerPackageBestEffort (): void
271323 {
272324 $ composer = $ this ->findComposerBinary ();
@@ -326,6 +378,46 @@ final class EvoBootstrapper
326378 $ this ->out ('PHP installer update: OK ' );
327379 }
328380
381+ private function updatePhpInstallerPackageGlobal (): void
382+ {
383+ $ composer = $ this ->findComposerBinary ();
384+ if ($ composer === null ) {
385+ $ this ->out ('PHP installer update: skipped (Composer not found). ' );
386+ return ;
387+ }
388+
389+ $ home = $ this ->resolveHomeDir ();
390+ $ env = [
391+ 'COMPOSER_ALLOW_SUPERUSER ' => '1 ' ,
392+ 'HOME ' => $ home ,
393+ ];
394+ $ composerHome = getenv ('COMPOSER_HOME ' );
395+ if (is_string ($ composerHome ) && trim ($ composerHome ) !== '' ) {
396+ $ env ['COMPOSER_HOME ' ] = $ composerHome ;
397+ }
398+
399+ $ this ->out ('Updating PHP installer package via Composer (global)... ' );
400+ $ code = $ this ->runProcess (
401+ [
402+ $ composer ,
403+ 'global ' ,
404+ 'update ' ,
405+ 'evolution-cms/installer ' ,
406+ '--no-interaction ' ,
407+ '--no-ansi ' ,
408+ '--no-progress ' ,
409+ ],
410+ null ,
411+ $ env
412+ );
413+ if ($ code !== 0 ) {
414+ $ this ->out ("PHP installer update: failed (exit code {$ code }). " );
415+ return ;
416+ }
417+
418+ $ this ->out ('PHP installer update: OK ' );
419+ }
420+
329421 private function findComposerBinary (): ?string
330422 {
331423 $ bins = ['composer ' , 'composer2 ' ];
@@ -356,6 +448,71 @@ final class EvoBootstrapper
356448 return null ;
357449 }
358450
451+ private function isInteractiveSession (): bool
452+ {
453+ if (function_exists ('stream_isatty ' )) {
454+ return @stream_isatty (STDIN );
455+ }
456+ if (function_exists ('posix_isatty ' )) {
457+ return @posix_isatty (STDIN );
458+ }
459+ return false ;
460+ }
461+
462+ private function isCiEnvironment (): bool
463+ {
464+ $ ci = getenv ('CI ' );
465+ return is_string ($ ci ) && trim ($ ci ) !== '' ;
466+ }
467+
468+ private function hasArg (string $ needle ): bool
469+ {
470+ foreach ($ this ->args as $ arg ) {
471+ if ($ arg === $ needle ) {
472+ return true ;
473+ }
474+ if (str_starts_with ($ needle , '-- ' ) && str_starts_with ($ arg , $ needle . '= ' )) {
475+ return true ;
476+ }
477+ }
478+ return false ;
479+ }
480+
481+ private function normalizeVersion (string $ version ): string
482+ {
483+ $ v = trim ($ version );
484+ $ v = ltrim ($ v , "vV " );
485+ return $ v ;
486+ }
487+
488+ private function isNewerVersion (string $ current , string $ latest ): bool
489+ {
490+ $ currentNorm = $ this ->normalizeVersion ($ current );
491+ $ latestNorm = $ this ->normalizeVersion ($ latest );
492+ if ($ currentNorm === '' || $ latestNorm === '' ) {
493+ return $ latest !== $ current ;
494+ }
495+ if (!preg_match ('/^[0-9]+( \\.[0-9]+)*/ ' , $ currentNorm ) || !preg_match ('/^[0-9]+( \\.[0-9]+)*/ ' , $ latestNorm )) {
496+ return $ latest !== $ current ;
497+ }
498+ return version_compare ($ latestNorm , $ currentNorm , '> ' );
499+ }
500+
501+ private function promptYesNo (string $ question , bool $ defaultYes ): bool
502+ {
503+ $ suffix = $ defaultYes ? ' ' : '' ;
504+ $ this ->out ($ question . $ suffix );
505+ $ line = fgets (STDIN );
506+ if ($ line === false ) {
507+ return $ defaultYes ;
508+ }
509+ $ answer = strtolower (trim ($ line ));
510+ if ($ answer === '' ) {
511+ return $ defaultYes ;
512+ }
513+ return in_array ($ answer , ['y ' , 'yes ' ], true );
514+ }
515+
359516 private function findComposerWorkDir (): ?string
360517 {
361518 $ candidate = $ this ->detectComposerRootFromVendorLayout ();
0 commit comments