124 lines
4.2 KiB
Ruby
Executable File
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)
|
|
|
|
# Old version directories linger after upgrades; diff the newest one
|
|
versions = Dir.glob(File.join(cache_base, '*')).select { |p| File.directory?(p) }
|
|
return false if versions.empty?
|
|
|
|
cache_path = versions.max_by { |p| File.mtime(p) }
|
|
|
|
# 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)
|