fantasy-cli 1.2.14 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,131 +1,153 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'singleton'
4
+
3
5
  module Gsd
4
6
  module Plugins
5
- # Plugin Hooks - Sistema de hooks para plugins
7
+ # Plugin Hooks - Sistema de hooks/pub-sub para plugins
8
+ #
9
+ # Implementa um sistema de eventos pub/sub onde plugins podem:
10
+ # - Se registrar para eventos específicos
11
+ # - Emitir eventos para outros plugins
12
+ # - Usar wildcards para capturar múltiplos eventos
6
13
  #
7
- # Responsável por:
8
- # - Registrar hooks de plugins
9
- # - Executar hooks em eventos
10
- # - Gerenciar prioridade de hooks
14
+ # @example
15
+ # hooks = Hooks.instance
16
+ # hooks.on('event.name') { |data| puts data }
17
+ # hooks.trigger('event.name', { foo: 'bar' })
11
18
  class Hooks
12
- attr_reader :hooks
13
-
14
- # Hook types
15
- HOOK_TYPES = [
16
- :pre_command,
17
- :post_command,
18
- :pre_tool,
19
- :post_tool,
20
- :pre_commit,
21
- :post_phase,
22
- :on_enable,
23
- :on_disable
24
- ].freeze
25
-
26
- # Inicializa o Hooks
19
+ include Singleton
20
+
21
+ attr_reader :handlers, :history
22
+
27
23
  def initialize
28
- @hooks = {}
29
- HOOK_TYPES.each { |type| @hooks[type] = [] }
24
+ @handlers = {}
25
+ @mutex = Mutex.new
26
+ @history = []
27
+ @history_limit = 100
30
28
  end
31
29
 
32
- # Registra um hook
30
+ # Registra um handler para um evento (API pub/sub)
33
31
  #
34
- # @param type [Symbol] Tipo do hook
35
- # @param plugin [String] Nome do plugin
36
- # @param callback [Proc] Callback para executar
37
- # @param priority [Integer] Prioridade (maior = primeiro)
38
- # @return [void]
39
- def register(type, plugin, callback, priority: 0)
40
- unless HOOK_TYPES.include?(type)
41
- raise ArgumentError, "Invalid hook type: #{type}"
32
+ # @param event [String, Symbol] Nome do evento (pode usar wildcards *)
33
+ # @param priority [Integer] Prioridade (maior = primeiro, default: 0)
34
+ # @param block [Proc] Handler
35
+ def on(event, priority: 0, &block)
36
+ @mutex.synchronize do
37
+ event = event.to_s
38
+ @handlers[event] ||= []
39
+ @handlers[event] << { block: block, priority: priority }
40
+ @handlers[event].sort_by! { |h| -h[:priority] }
42
41
  end
42
+ end
43
43
 
44
- @hooks[type] << {
45
- plugin: plugin,
46
- callback: callback,
47
- priority: priority,
48
- registered_at: Time.now
49
- }
50
-
51
- # Ordena por prioridade
52
- @hooks[type].sort_by! { |h| -h[:priority] }
44
+ # Emite um evento para todos os handlers registrados (alias para trigger)
45
+ def emit(event, *args)
46
+ trigger(event, *args)
53
47
  end
54
48
 
55
- # Remove hook de um plugin
56
- #
57
- # @param plugin [String] Nome do plugin
58
- # @return [void]
59
- def unregister(plugin)
60
- @hooks.each do |type, hook_list|
61
- @hooks[type] = hook_list.reject { |h| h[:plugin] == plugin }
62
- end
49
+ # Dispara hooks para um evento (alias para trigger)
50
+ def run(event, context = {})
51
+ trigger(event, context)
63
52
  end
64
53
 
65
- # Executa hooks de um tipo
54
+ # Emite um evento para todos os handlers registrados
66
55
  #
67
- # @param type [Symbol] Tipo do hook
68
- # @param context [Hash] Contexto para o hook
69
- # @return [Array] Resultados dos hooks
70
- def run(type, context = {})
71
- unless HOOK_TYPES.include?(type)
72
- raise ArgumentError, "Invalid hook type: #{type}"
73
- end
74
-
56
+ # @param event [String, Symbol] Nome do evento
57
+ # @param args [Array] Argumentos para os handlers
58
+ # @return [Array] Retornos de todos os handlers
59
+ def trigger(event, *args)
60
+ event = event.to_s
75
61
  results = []
76
- @hooks[type].each do |hook|
62
+
63
+ log_event(event, args)
64
+ handlers = matching_handlers(event)
65
+
66
+ handlers.each do |handler|
77
67
  begin
78
- result = hook[:callback].call(context)
79
- results << {
80
- plugin: hook[:plugin],
81
- result: result,
82
- success: true
83
- }
84
- rescue => e
85
- results << {
86
- plugin: hook[:plugin],
87
- error: e.message,
88
- success: false
89
- }
90
- warn "[Hooks] Error in #{type} hook (#{hook[:plugin]}): #{e.message}"
68
+ result = handler[:block].call(*args)
69
+ results << result
70
+ rescue StandardError => e
71
+ warn "[Hooks] Error in handler for '#{event}': #{e.message}"
91
72
  end
92
73
  end
93
74
 
94
75
  results
95
76
  end
96
77
 
97
- # Lista hooks registrados
98
- #
99
- # @param type [Symbol, nil] Tipo específico
100
- # @return [Array<Hash>] Lista de hooks
101
- def list(type = nil)
102
- if type
103
- @hooks[type] || []
104
- else
105
- @hooks
106
- end
78
+ # Verifica se há handlers para um evento
79
+ def has_handlers?(event)
80
+ event = event.to_s
81
+ !matching_handlers(event).empty?
107
82
  end
108
83
 
109
- # Conta hooks por tipo
110
- #
111
- # @return [Hash] Contagem por tipo
84
+ # Remove um handler (não implementado na API pub/sub)
85
+ def unregister(plugin)
86
+ @handlers.delete_if { |_, handlers| handlers.any? { |h| h[:plugin] == plugin } }
87
+ end
88
+
89
+ # Lista todos os eventos registrados
90
+ def events
91
+ @mutex.synchronize { @handlers.keys.dup }
92
+ end
93
+
94
+ # Conta handlers por evento
112
95
  def count
113
- @hooks.transform_values(&:count)
96
+ @handlers.transform_values(&:count)
114
97
  end
115
98
 
116
- # Limpa todos os hooks
117
- #
118
- # @return [void]
99
+ # Limpa todos os handlers
119
100
  def clear
120
- HOOK_TYPES.each { |type| @hooks[type] = [] }
101
+ @mutex.synchronize do
102
+ @handlers.clear
103
+ @history.clear
104
+ end
121
105
  end
122
106
 
123
- # Limpa hooks de um tipo
124
- #
125
- # @param type [Symbol] Tipo do hook
126
- # @return [void]
127
- def clear_type(type)
128
- @hooks[type] = [] if HOOK_TYPES.include?(type)
107
+ # Histórico de eventos
108
+ def event_history(event = nil)
109
+ if event
110
+ @history.select { |h| h[:event] == event.to_s }
111
+ else
112
+ @history.dup
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ def matching_handlers(event)
119
+ @mutex.synchronize do
120
+ handlers = []
121
+
122
+ @handlers.each do |pattern, pattern_handlers|
123
+ if match_pattern?(event, pattern)
124
+ handlers.concat(pattern_handlers)
125
+ end
126
+ end
127
+
128
+ handlers.uniq.sort_by { |h| -h[:priority] }
129
+ end
130
+ end
131
+
132
+ def match_pattern?(event, pattern)
133
+ return true if pattern == event
134
+ return true if pattern.end_with?('.*') && event.start_with?(pattern[0..-2])
135
+ return true if pattern.start_with?('*') && event.end_with?(pattern[1..-1])
136
+ return true if pattern.include?('*') && event =~ Regexp.new(pattern.gsub('.', '\.').gsub('*', '.*'))
137
+
138
+ false
139
+ end
140
+
141
+ def log_event(event, args)
142
+ @mutex.synchronize do
143
+ @history << {
144
+ event: event,
145
+ args: args.map { |a| a.respond_to?(:to_h) ? a.to_h : a.to_s },
146
+ timestamp: Time.now.to_f
147
+ }
148
+
149
+ @history.shift while @history.length > @history_limit
150
+ end
129
151
  end
130
152
  end
131
153
  end
@@ -0,0 +1,293 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'digest'
5
+
6
+ module Gsd
7
+ module Plugins
8
+ # HotReload - Sistema de hot-reload para plugins
9
+ #
10
+ # Monitora arquivos de plugins e recarrega automaticamente
11
+ # quando detecta mudanças.
12
+ class HotReload
13
+ attr_reader :watching, :check_interval, :last_check
14
+
15
+ def initialize(registry: nil, loader: nil, interval: 1.0)
16
+ @registry = registry || Registry.instance
17
+ @loader = loader || Loader.new(registry: @registry)
18
+ @check_interval = interval
19
+ @watched_files = {}
20
+ @file_hashes = {}
21
+ @watching = false
22
+ @watcher_thread = nil
23
+ @mutex = Mutex.new
24
+ @on_change_callbacks = []
25
+ end
26
+
27
+ # Inicia monitoramento
28
+ def start
29
+ return if @watching
30
+
31
+ @watching = true
32
+ scan_files
33
+
34
+ @watcher_thread = Thread.new do
35
+ while @watching
36
+ check_changes
37
+ sleep(@check_interval)
38
+ end
39
+ end
40
+
41
+ true
42
+ end
43
+
44
+ # Para monitoramento
45
+ def stop
46
+ @watching = false
47
+ @watcher_thread&.kill
48
+ @watcher_thread = nil
49
+ end
50
+
51
+ # Adiciona arquivo para monitoramento
52
+ def watch(path, plugin_name = nil)
53
+ @mutex.synchronize do
54
+ @watched_files[path] = {
55
+ plugin: plugin_name || extract_plugin_name(path),
56
+ last_modified: File.mtime(path),
57
+ hash: file_hash(path)
58
+ }
59
+ end
60
+ end
61
+
62
+ # Remove arquivo do monitoramento
63
+ def unwatch(path)
64
+ @mutex.synchronize do
65
+ @watched_files.delete(path)
66
+ end
67
+ end
68
+
69
+ # Verifica mudanças manualmente
70
+ def check_changes
71
+ changes = []
72
+
73
+ @mutex.synchronize do
74
+ @watched_files.each do |path, info|
75
+ next unless File.exist?(path)
76
+
77
+ current_mtime = File.mtime(path)
78
+ current_hash = file_hash(path)
79
+
80
+ if current_mtime > info[:last_modified] || current_hash != info[:hash]
81
+ changes << {
82
+ path: path,
83
+ plugin: info[:plugin],
84
+ old_mtime: info[:last_modified],
85
+ new_mtime: current_mtime
86
+ }
87
+
88
+ # Atualiza info
89
+ @watched_files[path][:last_modified] = current_mtime
90
+ @watched_files[path][:hash] = current_hash
91
+ end
92
+ end
93
+ end
94
+
95
+ # Processa mudanças
96
+ changes.each do |change|
97
+ handle_change(change)
98
+ end
99
+
100
+ changes
101
+ end
102
+
103
+ # Registra callback para mudanças
104
+ def on_change(&block)
105
+ @on_change_callbacks << block
106
+ end
107
+
108
+ # Lista arquivos sendo monitorados
109
+ def watched
110
+ @mutex.synchronize { @watched_files.dup }
111
+ end
112
+
113
+ # Recarrega um plugin específico
114
+ def reload_plugin(name)
115
+ return false unless @registry.exists?(name)
116
+
117
+ begin
118
+ @loader.reload(name)
119
+ trigger_change_callbacks(:reloaded, name)
120
+ true
121
+ rescue StandardError => e
122
+ warn "[HotReload] Failed to reload plugin '#{name}': #{e.message}"
123
+ false
124
+ end
125
+ end
126
+
127
+ # Força recarregamento de todos os plugins
128
+ def reload_all
129
+ @registry.all.each do |plugin|
130
+ reload_plugin(plugin.name)
131
+ end
132
+ end
133
+
134
+ # Pausa monitoramento temporariamente
135
+ def pause
136
+ @paused = true
137
+ end
138
+
139
+ # Retoma monitoramento
140
+ def resume
141
+ @paused = false
142
+ end
143
+
144
+ # Status atual
145
+ def status
146
+ {
147
+ watching: @watching,
148
+ paused: @paused,
149
+ files_watched: @watched_files.length,
150
+ last_check: @last_check
151
+ }
152
+ end
153
+
154
+ private
155
+
156
+ def scan_files
157
+ return unless Dir.exist?(@registry.plugins_dir)
158
+
159
+ Dir.glob(File.join(@registry.plugins_dir, '*', 'plugin.rb')).each do |path|
160
+ watch(path)
161
+ end
162
+ end
163
+
164
+ def file_hash(path)
165
+ return nil unless File.exist?(path)
166
+
167
+ Digest::MD5.file(path).hexdigest
168
+ rescue StandardError
169
+ nil
170
+ end
171
+
172
+ def extract_plugin_name(path)
173
+ File.basename(File.dirname(path))
174
+ end
175
+
176
+ def handle_change(change)
177
+ return if @paused
178
+
179
+ @last_check = Time.now
180
+
181
+ plugin_name = change[:plugin]
182
+ return unless plugin_name
183
+
184
+ # Notifica callbacks
185
+ trigger_change_callbacks(:detected, plugin_name, change[:path])
186
+
187
+ # Recarrega plugin
188
+ reload_plugin(plugin_name)
189
+ end
190
+
191
+ def trigger_change_callbacks(type, plugin_name, path = nil)
192
+ @on_change_callbacks.each do |callback|
193
+ begin
194
+ callback.call(type, plugin_name, path)
195
+ rescue StandardError => e
196
+ warn "[HotReload] Error in change callback: #{e.message}"
197
+ end
198
+ end
199
+ end
200
+ end
201
+
202
+ # FileWatcher - Watcher simples para arquivos individuais
203
+ class FileWatcher
204
+ def initialize(path, &callback)
205
+ @path = path
206
+ @callback = callback
207
+ @last_mtime = File.exist?(path) ? File.mtime(path) : nil
208
+ @running = false
209
+ end
210
+
211
+ def start(interval: 1.0)
212
+ return if @running
213
+
214
+ @running = true
215
+
216
+ Thread.new do
217
+ while @running
218
+ check
219
+ sleep(interval)
220
+ end
221
+ end
222
+ end
223
+
224
+ def stop
225
+ @running = false
226
+ end
227
+
228
+ def check
229
+ return unless File.exist?(@path)
230
+
231
+ current_mtime = File.mtime(@path)
232
+
233
+ if @last_mtime && current_mtime > @last_mtime
234
+ @callback.call(@path, @last_mtime, current_mtime)
235
+ end
236
+
237
+ @last_mtime = current_mtime
238
+ end
239
+ end
240
+
241
+ # DebouncedWatcher - Watcher com debounce para evitar múltiplos reloads
242
+ class DebouncedWatcher
243
+ def initialize(registry:, debounce_time: 0.5)
244
+ @registry = registry
245
+ @debounce_time = debounce_time
246
+ @pending_changes = {}
247
+ @mutex = Mutex.new
248
+ end
249
+
250
+ def notify_change(plugin_name, path)
251
+ @mutex.synchronize do
252
+ @pending_changes[plugin_name] = {
253
+ path: path,
254
+ time: Time.now
255
+ }
256
+ end
257
+
258
+ # Inicia timer se não estiver rodando
259
+ start_debounce_timer
260
+ end
261
+
262
+ private
263
+
264
+ def start_debounce_timer
265
+ Thread.new do
266
+ sleep(@debounce_time)
267
+ process_pending
268
+ end
269
+ end
270
+
271
+ def process_pending
272
+ changes = []
273
+
274
+ @mutex.synchronize do
275
+ now = Time.now
276
+ @pending_changes.each do |plugin, info|
277
+ if now - info[:time] >= @debounce_time
278
+ changes << plugin
279
+ end
280
+ end
281
+
282
+ # Remove processados
283
+ changes.each { |p| @pending_changes.delete(p) }
284
+ end
285
+
286
+ # Recarrega plugins
287
+ changes.each do |plugin_name|
288
+ HotReload.new.reload_plugin(plugin_name)
289
+ end
290
+ end
291
+ end
292
+ end
293
+ end