@@ -52,6 +52,7 @@ public function configure(): void
5252 $ this ->addOption ( 'migrations-only ' , 'm ' , false , 'Only copy new migrations ' );
5353 $ this ->addOption ( 'skip-views ' , null , false , 'Skip updating view files ' );
5454 $ this ->addOption ( 'force-views ' , null , false , 'Overwrite existing view files with package versions (destroys local view customizations) ' );
55+ $ this ->addOption ( 'prompt-views ' , null , false , 'Prompt before overwriting each view that is newer in the package and differs from the local copy ' );
5556 $ this ->addOption ( 'skip-migrations ' , null , false , 'Skip copying migrations ' );
5657 $ this ->addOption ( 'run-migrations ' , 'r ' , false , 'Run migrations automatically after copying ' );
5758 }
@@ -84,10 +85,12 @@ public function execute( array $parameters = [] ): int
8485 // Check for updates
8586 $ hasUpdates = $ this ->checkForUpdates ();
8687
87- // --force-views always has work to do (it re-publishes package views).
88- $ forceViews = (bool ) $ this ->input ->getOption ( 'force-views ' );
88+ // --force-views / --prompt-views always have potential work to do: they
89+ // re-examine published views even when the manifest version is unchanged.
90+ $ forceViews = (bool ) $ this ->input ->getOption ( 'force-views ' );
91+ $ promptViews = (bool ) $ this ->input ->getOption ( 'prompt-views ' );
8992
90- if ( !$ hasUpdates && !$ forceViews )
93+ if ( !$ hasUpdates && !$ forceViews && ! $ promptViews )
9194 {
9295 $ this ->output ->success ( "✓ CMS is already up to date! " );
9396 return 0 ;
@@ -97,6 +100,10 @@ public function execute( array $parameters = [] ): int
97100 {
98101 $ this ->output ->writeln ( " ⚠️ --force-views: existing view files will be overwritten with package versions " );
99102 }
103+ elseif ( $ promptViews )
104+ {
105+ $ this ->output ->writeln ( " ℹ️ --prompt-views: you will be asked before overwriting each changed view " );
106+ }
100107
101108 // If --check flag, exit after displaying what would be updated
102109 if ( $ this ->input ->getOption ( 'check ' ) )
@@ -486,6 +493,29 @@ private function updateViews(): bool
486493 }
487494
488495 $ force = (bool ) $ this ->input ->getOption ( 'force-views ' );
496+ $ prompt = (bool ) $ this ->input ->getOption ( 'prompt-views ' );
497+
498+ // Interactive merge: add new views, and ask per file for views that are
499+ // newer in the package and differ from the local copy.
500+ if ( $ prompt && !$ force )
501+ {
502+ $ copied = $ this ->copyViewsInteractive ( $ viewSource , $ viewDest );
503+
504+ if ( $ copied > 0 )
505+ {
506+ $ this ->output ->writeln ( "\n Copied $ copied view file " . ( $ copied !== 1 ? 's ' : '' ) );
507+ }
508+ else
509+ {
510+ $ this ->output ->writeln ( " No view files copied " );
511+ }
512+
513+ $ this ->output ->writeln ( " ℹ️ Unchanged and declined views were left as-is " );
514+ $ this ->output ->writeln ( " Package views location: " . $ viewSource . "/ " );
515+
516+ return true ;
517+ }
518+
489519 $ copied = $ this ->copyNewViews ( $ viewSource , $ viewDest , $ force );
490520
491521 if ( $ copied > 0 )
@@ -583,6 +613,120 @@ private function copyNewViews( string $source, string $dest, bool $force = false
583613 return $ copied ;
584614 }
585615
616+ /**
617+ * Recursively copy views, prompting before overwriting changed files.
618+ *
619+ * Missing views are added automatically. An existing view is only offered
620+ * for overwrite when the package copy is newer (by modification time) AND
621+ * its contents differ from the local copy, so identical or locally-newer
622+ * files are skipped without noise.
623+ *
624+ * @param string $source Source directory
625+ * @param string $dest Destination directory
626+ * @return int Number of files copied
627+ */
628+ private function copyViewsInteractive ( string $ source , string $ dest ): int
629+ {
630+ $ items = scandir ( $ source );
631+
632+ if ( $ items === false )
633+ {
634+ return 0 ;
635+ }
636+
637+ $ copied = 0 ;
638+
639+ foreach ( $ items as $ item )
640+ {
641+ if ( $ item === '. ' || $ item === '.. ' )
642+ {
643+ continue ;
644+ }
645+
646+ $ sourcePath = $ source . '/ ' . $ item ;
647+ $ destPath = $ dest . '/ ' . $ item ;
648+
649+ if ( is_dir ( $ sourcePath ) )
650+ {
651+ $ copied += $ this ->copyViewsInteractive ( $ sourcePath , $ destPath );
652+ continue ;
653+ }
654+
655+ $ relative = ltrim ( str_replace ( $ this ->_projectPath , '' , $ destPath ), '/ ' );
656+
657+ // New view: add without prompting.
658+ if ( !file_exists ( $ destPath ) )
659+ {
660+ if ( $ this ->copyViewFile ( $ sourcePath , $ destPath ) )
661+ {
662+ $ this ->output ->writeln ( " ✓ Added: $ relative " );
663+ $ this ->_messages [] = "Added view: $ relative " ;
664+ $ copied ++;
665+ }
666+
667+ continue ;
668+ }
669+
670+ // Only consider views the package updated more recently.
671+ if ( filemtime ( $ sourcePath ) <= filemtime ( $ destPath ) )
672+ {
673+ continue ;
674+ }
675+
676+ // Skip when contents are identical despite the newer timestamp.
677+ if ( md5_file ( $ sourcePath ) === md5_file ( $ destPath ) )
678+ {
679+ continue ;
680+ }
681+
682+ $ packageDate = date ( 'Y-m-d H:i ' , (int ) filemtime ( $ sourcePath ) );
683+ $ localDate = date ( 'Y-m-d H:i ' , (int ) filemtime ( $ destPath ) );
684+
685+ $ this ->output ->writeln ( " • $ relative (package $ packageDate is newer than local $ localDate) " );
686+
687+ if ( !$ this ->confirm ( " Overwrite this view? " , false ) )
688+ {
689+ $ this ->output ->writeln ( " – Skipped: $ relative " );
690+ continue ;
691+ }
692+
693+ if ( $ this ->copyViewFile ( $ sourcePath , $ destPath ) )
694+ {
695+ $ this ->output ->writeln ( " ✓ Updated: $ relative " );
696+ $ this ->_messages [] = "Updated view: $ relative " ;
697+ $ copied ++;
698+ }
699+ }
700+
701+ return $ copied ;
702+ }
703+
704+ /**
705+ * Copy a single view file, creating the destination directory as needed.
706+ *
707+ * @param string $source Source file
708+ * @param string $dest Destination file
709+ * @return bool
710+ */
711+ private function copyViewFile ( string $ source , string $ dest ): bool
712+ {
713+ $ destDir = dirname ( $ dest );
714+
715+ if ( !is_dir ( $ destDir ) && !mkdir ( $ destDir , 0755 , true ) && !is_dir ( $ destDir ) )
716+ {
717+ $ this ->output ->error ( " ✗ Failed to create directory: $ destDir " );
718+ return false ;
719+ }
720+
721+ if ( !copy ( $ source , $ dest ) )
722+ {
723+ $ this ->output ->error ( " ✗ Failed to copy: $ dest " );
724+ return false ;
725+ }
726+
727+ return true ;
728+ }
729+
586730 /**
587731 * Scaffold the scheduled jobs configuration.
588732 *
0 commit comments