cc-os/bin/refresh-plugins

124 lines
4.2 KiB
Ruby
Executable File

#!/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)