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.
- checksums.yaml +4 -4
- data/README.md +8 -1
- data/lib/gsd/agents/communication.rb +260 -0
- data/lib/gsd/agents/swarm.rb +341 -0
- data/lib/gsd/lsp/client.rb +394 -0
- data/lib/gsd/lsp/completion.rb +266 -0
- data/lib/gsd/lsp/diagnostics.rb +259 -0
- data/lib/gsd/lsp/hover.rb +244 -0
- data/lib/gsd/lsp/protocol.rb +434 -0
- data/lib/gsd/lsp/server_manager.rb +290 -0
- data/lib/gsd/lsp/symbols.rb +368 -0
- data/lib/gsd/plugins/api.rb +340 -0
- data/lib/gsd/plugins/hooks.rb +117 -95
- data/lib/gsd/plugins/hot_reload.rb +293 -0
- data/lib/gsd/plugins/registry.rb +273 -0
- data/lib/gsd/tui/agent_panel.rb +182 -0
- data/lib/gsd/tui/animations.rb +320 -0
- data/lib/gsd/tui/app.rb +442 -2
- data/lib/gsd/tui/colors.rb +15 -0
- data/lib/gsd/tui/effects.rb +263 -0
- data/lib/gsd/tui/header.rb +13 -5
- data/lib/gsd/tui/input_box.rb +10 -7
- data/lib/gsd/tui/mouse.rb +388 -0
- data/lib/gsd/tui/persistence.rb +192 -0
- data/lib/gsd/tui/session.rb +273 -0
- data/lib/gsd/tui/status_bar.rb +63 -15
- data/lib/gsd/tui/tab.rb +112 -0
- data/lib/gsd/tui/tab_manager.rb +191 -0
- data/lib/gsd/tui/transitions.rb +262 -0
- data/lib/gsd/version.rb +1 -1
- metadata +22 -1
data/lib/gsd/plugins/hooks.rb
CHANGED
|
@@ -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
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
@
|
|
29
|
-
|
|
24
|
+
@handlers = {}
|
|
25
|
+
@mutex = Mutex.new
|
|
26
|
+
@history = []
|
|
27
|
+
@history_limit = 100
|
|
30
28
|
end
|
|
31
29
|
|
|
32
|
-
# Registra um
|
|
30
|
+
# Registra um handler para um evento (API pub/sub)
|
|
33
31
|
#
|
|
34
|
-
# @param
|
|
35
|
-
# @param
|
|
36
|
-
# @param
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
#
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
#
|
|
54
|
+
# Emite um evento para todos os handlers registrados
|
|
66
55
|
#
|
|
67
|
-
# @param
|
|
68
|
-
# @param
|
|
69
|
-
# @return [Array]
|
|
70
|
-
def
|
|
71
|
-
|
|
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
|
-
|
|
62
|
+
|
|
63
|
+
log_event(event, args)
|
|
64
|
+
handlers = matching_handlers(event)
|
|
65
|
+
|
|
66
|
+
handlers.each do |handler|
|
|
77
67
|
begin
|
|
78
|
-
result =
|
|
79
|
-
results <<
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
#
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
@
|
|
96
|
+
@handlers.transform_values(&:count)
|
|
114
97
|
end
|
|
115
98
|
|
|
116
|
-
# Limpa todos os
|
|
117
|
-
#
|
|
118
|
-
# @return [void]
|
|
99
|
+
# Limpa todos os handlers
|
|
119
100
|
def clear
|
|
120
|
-
|
|
101
|
+
@mutex.synchronize do
|
|
102
|
+
@handlers.clear
|
|
103
|
+
@history.clear
|
|
104
|
+
end
|
|
121
105
|
end
|
|
122
106
|
|
|
123
|
-
#
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|