@@ -187,6 +187,7 @@ impl BundlerRuntime {
187187 }
188188
189189 /// Check if bundler environment is synchronized (dependencies satisfied)
190+ /// Also updates Gemfile.lock if check passes to handle removed gems
190191 pub fn check_sync (
191192 & self ,
192193 butler_runtime : & crate :: butler:: ButlerRuntime ,
@@ -209,6 +210,13 @@ impl BundlerRuntime {
209210 is_synced,
210211 output. status. code( ) . unwrap_or( -1 )
211212 ) ;
213+
214+ // If check passes, update lockfile to handle removed gems
215+ if is_synced {
216+ debug ! ( "Bundle check passed, updating lockfile to match Gemfile" ) ;
217+ self . update_lockfile_quietly ( butler_runtime) ?;
218+ }
219+
212220 Ok ( is_synced)
213221 }
214222 Err ( e) => {
@@ -344,21 +352,103 @@ impl BundlerRuntime {
344352 }
345353 }
346354
355+ /// Update Gemfile.lock to match Gemfile quietly (no output)
356+ /// Used by check_sync to ensure lockfile is up to date
357+ fn update_lockfile_quietly (
358+ & self ,
359+ butler_runtime : & crate :: butler:: ButlerRuntime ,
360+ ) -> std:: io:: Result < ( ) > {
361+ debug ! ( "Quietly updating Gemfile.lock to match Gemfile" ) ;
362+
363+ // Run bundle lock --local to regenerate lockfile based on Gemfile
364+ // Uses --local to avoid network access since bundle check already passed
365+ let output = Command :: new ( "bundle" )
366+ . arg ( "lock" )
367+ . arg ( "--local" )
368+ . current_dir ( & self . root )
369+ . output_with_context ( butler_runtime) ?;
370+
371+ if output. status . success ( ) {
372+ debug ! ( "Gemfile.lock updated successfully" ) ;
373+ Ok ( ( ) )
374+ } else {
375+ // Silently ignore errors - lockfile update is best-effort
376+ // The bundle check already passed, so environment is functional
377+ debug ! (
378+ "Bundle lock failed but continuing (exit code: {})" ,
379+ output. status. code( ) . unwrap_or( -1 )
380+ ) ;
381+ Ok ( ( ) )
382+ }
383+ }
384+
385+ /// Update Gemfile.lock to match Gemfile (handles removed gems)
386+ /// Used by sync command with output streaming
387+ fn update_lockfile < F > (
388+ & self ,
389+ butler_runtime : & crate :: butler:: ButlerRuntime ,
390+ output_handler : & mut F ,
391+ ) -> std:: io:: Result < ( ) >
392+ where
393+ F : FnMut ( & str ) ,
394+ {
395+ debug ! ( "Updating Gemfile.lock to match Gemfile" ) ;
396+
397+ // Run bundle lock --local to regenerate lockfile based on Gemfile
398+ // Uses --local to avoid network access since bundle check already passed
399+ let output = Command :: new ( "bundle" )
400+ . arg ( "lock" )
401+ . arg ( "--local" )
402+ . current_dir ( & self . root )
403+ . output_with_context ( butler_runtime) ?;
404+
405+ // Stream output to handler
406+ if !output. stdout . is_empty ( ) {
407+ let stdout_str = String :: from_utf8_lossy ( & output. stdout ) ;
408+ for line in stdout_str. lines ( ) {
409+ output_handler ( line) ;
410+ }
411+ }
412+
413+ if !output. stderr . is_empty ( ) {
414+ let stderr_str = String :: from_utf8_lossy ( & output. stderr ) ;
415+ for line in stderr_str. lines ( ) {
416+ eprintln ! ( "{}" , line) ;
417+ }
418+ }
419+
420+ if output. status . success ( ) {
421+ debug ! ( "Gemfile.lock updated successfully" ) ;
422+ Ok ( ( ) )
423+ } else {
424+ Err ( std:: io:: Error :: other ( format ! (
425+ "Bundle lock failed (exit code: {})" ,
426+ output. status. code( ) . unwrap_or( -1 )
427+ ) ) )
428+ }
429+ }
430+
347431 /// Synchronize the bundler environment (configure path, check, and install if needed)
348432 pub fn synchronize < F > (
349433 & self ,
350434 butler_runtime : & crate :: butler:: ButlerRuntime ,
351- output_handler : F ,
435+ mut output_handler : F ,
352436 ) -> std:: io:: Result < SyncResult >
353437 where
354438 F : FnMut ( & str ) ,
355439 {
356440 debug ! ( "Starting bundler synchronization" ) ;
357441
358442 // Step 1: Check if already synchronized
443+ // Note: check_sync already updates lockfile quietly, but for sync command
444+ // we want to show output, so we call update_lockfile explicitly
359445 match self . check_sync ( butler_runtime) ? {
360446 true => {
361447 debug ! ( "Bundler environment already synchronized" ) ;
448+
449+ // For sync command, show the lockfile update output
450+ self . update_lockfile ( butler_runtime, & mut output_handler) ?;
451+
362452 Ok ( SyncResult :: AlreadySynced )
363453 }
364454 false => {
0 commit comments