55require 'pathname'
66require 'fileutils'
77require 'open3'
8+ require 'find'
89
910module DemoScripts
1011 # Manages swapping dependencies between production and local/GitHub versions
@@ -132,6 +133,52 @@ def kill_watch_processes
132133 end
133134 # rubocop:enable Metrics/MethodLength
134135
136+ # CLI entry point: Display cache information including location, size, and cached repositories
137+ def show_cache_info
138+ unless File . directory? ( CACHE_DIR )
139+ puts 'ℹ️ Cache directory does not exist'
140+ puts " Location: #{ CACHE_DIR } "
141+ return
142+ end
143+
144+ # Get all repo directories once to avoid race conditions
145+ repo_dirs = cache_repo_dirs
146+
147+ # Calculate cache size and count repos
148+ repo_info = repo_dirs . map do |path |
149+ { path : path , basename : File . basename ( path ) , size : directory_size ( path ) }
150+ end
151+
152+ total_size = repo_info . sum { |info | info [ :size ] }
153+
154+ puts '📊 Cache information:'
155+ puts " Location: #{ CACHE_DIR } "
156+ puts " Repositories: #{ repo_info . count } "
157+ puts " Total size: #{ human_readable_size ( total_size ) } "
158+
159+ return unless repo_info . any?
160+
161+ puts "\n Cached repositories:"
162+ repo_info . each do |info |
163+ puts " - #{ info [ :basename ] } (#{ human_readable_size ( info [ :size ] ) } )"
164+ end
165+ end
166+
167+ # CLI entry point: Remove cached GitHub repositories
168+ # @param gem_name [String, nil] Optional gem name to clean specific gem cache, or nil to clean all
169+ def clean_cache ( gem_name : nil )
170+ unless File . directory? ( CACHE_DIR )
171+ puts 'ℹ️ Cache directory does not exist - nothing to clean'
172+ return
173+ end
174+
175+ if gem_name
176+ clean_gem_cache ( gem_name )
177+ else
178+ clean_all_cache
179+ end
180+ end
181+
135182 def load_config ( config_file )
136183 return unless File . exist? ( config_file )
137184
@@ -156,6 +203,126 @@ def load_config(config_file)
156203
157204 private
158205
206+ def cache_repo_dirs
207+ return [ ] unless File . directory? ( CACHE_DIR )
208+
209+ Dir . glob ( File . join ( CACHE_DIR , '*' ) ) . select do |path |
210+ File . directory? ( path ) && File . basename ( path ) != 'watch_logs'
211+ end
212+ end
213+
214+ # Match gem name in cache directory pattern: {org}-{gem}-{branch}
215+ # This ensures we match the repository component, not the org or branch
216+ def matches_gem_cache_pattern? ( basename , gem_name )
217+ # Normalize gem name for both underscore and hyphen variants
218+ normalized_gem = gem_name . tr ( '_' , '-' )
219+
220+ # Match the middle component after the first hyphen
221+ # Pattern: ^{org}-{gem}-{branch}$
222+ # This prevents false positives like matching "test" in "test-user-repo-branch"
223+ basename . match? ( /\A [^-]+-#{ Regexp . escape ( normalized_gem ) } -/ ) ||
224+ basename . match? ( /\A [^-]+-#{ Regexp . escape ( gem_name ) } -/ )
225+ end
226+
227+ def directory_size ( path )
228+ size = 0
229+ Find . find ( path ) do |file_path |
230+ # Skip symlinks to avoid circular references and incorrect sizes
231+ if File . symlink? ( file_path )
232+ Find . prune
233+ next
234+ end
235+
236+ size += File . size ( file_path ) if File . file? ( file_path )
237+ end
238+ size
239+ rescue Errno ::EACCES => e
240+ warn " ⚠️ Warning: Permission denied accessing #{ path } : #{ e . message } " if verbose
241+ 0
242+ rescue Errno ::ENOENT => e
243+ warn " ⚠️ Warning: Path not found #{ path } : #{ e . message } " if verbose
244+ 0
245+ rescue StandardError => e
246+ warn " ⚠️ Warning: Error calculating size for #{ path } : #{ e . message } " if verbose
247+ 0
248+ end
249+
250+ def human_readable_size ( bytes )
251+ units = %w[ B KB MB GB TB ]
252+ return "0 #{ units [ 0 ] } " if bytes . zero?
253+
254+ exp = ( Math . log ( bytes ) / Math . log ( 1024 ) ) . to_i
255+ exp = [ exp , units . length - 1 ] . min
256+ size = bytes . to_f / ( 1024 **exp )
257+ format ( '%<size>.2f %<unit>s' , size : size , unit : units [ exp ] )
258+ end
259+
260+ # rubocop:disable Metrics/MethodLength
261+ def clean_gem_cache ( gem_name )
262+ # Validate gem name to prevent path traversal
263+ unless gem_name . match? ( /\A [\w .-]+\z / )
264+ raise Error ,
265+ "Invalid gem name: #{ gem_name } . Only alphanumeric characters, hyphens, underscores, and dots allowed."
266+ end
267+
268+ # Find all cached repos for this gem
269+ # Expected format: {org}-{repo}-{branch} (e.g., shakacode-shakapacker-main)
270+ matching_dirs = cache_repo_dirs . select do |path |
271+ matches_gem_cache_pattern? ( File . basename ( path ) , gem_name )
272+ end
273+
274+ if matching_dirs . empty?
275+ puts "ℹ️ No cached repositories found for: #{ gem_name } "
276+ return
277+ end
278+
279+ puts "🗑️ Cleaning cache for #{ gem_name } ..."
280+ matching_dirs . each do |dir |
281+ size = directory_size ( dir )
282+ basename = File . basename ( dir )
283+ if dry_run
284+ puts " [DRY-RUN] Would remove #{ basename } (#{ human_readable_size ( size ) } )"
285+ else
286+ FileUtils . rm_rf ( dir )
287+ puts " ✓ Removed #{ basename } (#{ human_readable_size ( size ) } )"
288+ end
289+ end
290+ end
291+ # rubocop:enable Metrics/MethodLength
292+
293+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
294+ def clean_all_cache
295+ # Get all repo directories (exclude watch_logs)
296+ repo_dirs = cache_repo_dirs
297+
298+ if repo_dirs . empty?
299+ puts 'ℹ️ Cache is empty - nothing to clean'
300+ return
301+ end
302+
303+ # Calculate sizes once to avoid redundant directory traversal
304+ repo_info = repo_dirs . map do |dir |
305+ { path : dir , basename : File . basename ( dir ) , size : directory_size ( dir ) }
306+ end
307+
308+ total_size = repo_info . sum { |info | info [ :size ] }
309+ puts "🗑️ Cleaning entire cache (#{ repo_info . count } repositories, #{ human_readable_size ( total_size ) } )..."
310+
311+ if dry_run
312+ puts ' [DRY-RUN] Would remove:'
313+ repo_info . each do |info |
314+ puts " - #{ info [ :basename ] } (#{ human_readable_size ( info [ :size ] ) } )"
315+ end
316+ else
317+ repo_info . each do |info |
318+ FileUtils . rm_rf ( info [ :path ] )
319+ puts " ✓ Removed #{ info [ :basename ] } (#{ human_readable_size ( info [ :size ] ) } )"
320+ end
321+ puts "✅ Cleaned cache - freed #{ human_readable_size ( total_size ) } "
322+ end
323+ end
324+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
325+
159326 def load_watch_pids
160327 return { } unless File . exist? ( WATCH_PIDS_FILE )
161328
0 commit comments