#!/usr/bin/env ruby # frozen_string_literal: true # Refresh Claude Code plugin caches for directory-source marketplaces. # Discovers installed plugins from each local marketplace and refreshes their caches. # # Usage: # bin/refresh-plugins [marketplace-names...] # bin/refresh-plugins # refresh all local marketplaces # bin/refresh-plugins local-plugins # refresh only local-plugins # bin/refresh-plugins cc-plugins local-plugins require 'json' require 'pathname' class PluginRefresher attr_reader :results def initialize @results = [] end def refresh(marketplace_names = nil) marketplaces = load_marketplaces(marketplace_names) marketplaces.each do |name, config| next unless config['source']['source'] == 'directory' puts "Refreshing marketplace: #{name}" refresh_marketplace(name, config) puts end report_results end private def load_marketplaces(names) all = JSON.parse(File.read(File.expand_path('~/.claude/plugins/known_marketplaces.json'))) return all unless names all.select { |k, _| names.include?(k) } end def refresh_marketplace(marketplace_name, config) source_path = config['source']['path'] installed = load_installed_plugins(marketplace_name) installed.each do |plugin_name| refresh_plugin(marketplace_name, plugin_name, source_path) end end def load_installed_plugins(marketplace_name) all = JSON.parse(File.read(File.expand_path('~/.claude/plugins/installed_plugins.json'))) all['plugins'] .select { |k, _| k.end_with?("@#{marketplace_name}") } .keys .map { |k| k.sub(/@#{marketplace_name}$/, '') } .uniq end def refresh_plugin(marketplace_name, plugin_name, source_path) cache_exists = cache_exists?(marketplace_name, plugin_name) unless File.exist?(File.join(source_path, plugin_name)) @results << [plugin_name, "source not found at #{source_path}", :error] puts " #{plugin_name}: ERROR — source not found" return end system("claude plugin uninstall #{plugin_name}@#{marketplace_name} > /dev/null 2>&1") system("claude plugin install #{plugin_name}@#{marketplace_name} > /dev/null 2>&1") if verify_cache(marketplace_name, plugin_name, source_path) @results << [plugin_name, "refreshed", :success] puts " #{plugin_name}: REFRESHED" elsif !cache_exists @results << [plugin_name, "in-sync (not previously cached)", :info] puts " #{plugin_name}: IN SYNC (new)" else @results << [plugin_name, "STALE", :warning] puts " #{plugin_name}: STALE (differences remain)" end end def cache_exists?(marketplace_name, plugin_name) Dir.exist?(File.expand_path("~/.claude/plugins/cache/#{marketplace_name}/#{plugin_name}")) end def verify_cache(marketplace_name, plugin_name, source_path) cache_base = File.expand_path("~/.claude/plugins/cache/#{marketplace_name}/#{plugin_name}") source_full = File.join(source_path, plugin_name) # Find the version directory in cache versions = Dir.glob(File.join(cache_base, '*')).select { |p| File.directory?(p) } return false if versions.empty? cache_path = versions.first # Run diff check (exclude plugin manager state files) output = `diff -rq "#{cache_path}" "#{source_full}" --exclude='.git' --exclude='__pycache__' --exclude='.orphaned_at' 2>&1` output.strip.empty? end def report_results puts "\n=== Summary ===" successes = @results.select { |_, _, status| status == :success } warnings = @results.select { |_, _, status| status == :warning } errors = @results.select { |_, _, status| status == :error } infos = @results.select { |_, _, status| status == :info } puts "Refreshed: #{successes.map { |r| r[0] }.join(', ')}" if successes.any? puts "In sync: #{infos.map { |r| r[0] }.join(', ')}" if infos.any? puts "Stale (not fixed): #{warnings.map { |r| r[0] }.join(', ')}" if warnings.any? puts "Errors: #{errors.map { |r| "#{r[0]} (#{r[1]})" }.join(', ')}" if errors.any? puts "All plugins synchronized." if @results.all? { |_, _, s| s != :warning && s != :error } end end marketplace_names = ARGV.empty? ? nil : ARGV PluginRefresher.new.refresh(marketplace_names)