legionio 1.5.10 → 1.5.11
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/CHANGELOG.md +13 -0
- data/legionio.gemspec +4 -2
- data/lib/legion/cli/debug_command.rb +428 -0
- data/lib/legion/cli/update_command.rb +43 -5
- data/lib/legion/cli.rb +4 -0
- data/lib/legion/service.rb +5 -0
- data/lib/legion/version.rb +1 -1
- metadata +34 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2e6a0551564a834f76d460cb9ffed6cbc5e48e6dfe15a377c64e3a2c0540ac99
|
|
4
|
+
data.tar.gz: 0daf3944544ddcc630fdba40b35b8430b968db898c5dbfcfb9b348cee752ba48
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7bfab92636665b30da8655966c8d88a6bc112da34f96eae8968118917e0c7ca525ba8bf59ac98b62ede1cc41e704cb1b57707e9b4ff90e625ffe2bb6237111a5
|
|
7
|
+
data.tar.gz: 210b3a406307fed54591b2bc3bda9e0a35f8af8cad45788d1d1137d40db8b61637c2450b74b18a772d6065f3451031ca9d07ad3722ea7d209e1c22907afe120f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Legion Changelog
|
|
2
2
|
|
|
3
|
+
## [1.5.11] - 2026-03-25
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `legionio debug` command — full diagnostic dump (16 sections: versions, doctor, config, gems, extensions, RBAC, LLM, GAIA, transport, events, Apollo, remote/local Redis, PostgreSQL, RabbitMQ, API health) output as markdown or JSON, suitable for piping to an LLM session
|
|
7
|
+
- `legionio update --cleanup` flag — removes old gem versions after update via `Gem::Uninstaller` (default: no cleanup)
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- `update_command.rb` `snapshot_versions` now uses `find_all_by_name` + max version instead of `find_by_name`, which returned the already-activated (potentially stale) gem version
|
|
11
|
+
- `service.rb` `setup_api` guard prevents duplicate Puma start when `@api_thread` is already alive
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Bumped gemspec dependencies: legion-data >= 1.5.3, legion-gaia >= 0.9.24, legion-llm >= 0.5.8, legion-tty >= 0.4.35
|
|
15
|
+
|
|
3
16
|
## [1.5.10] - 2026-03-25
|
|
4
17
|
|
|
5
18
|
### Changed
|
data/legionio.gemspec
CHANGED
|
@@ -54,12 +54,14 @@ Gem::Specification.new do |spec|
|
|
|
54
54
|
|
|
55
55
|
spec.add_dependency 'legion-cache', '>= 1.3.16'
|
|
56
56
|
spec.add_dependency 'legion-crypt', '>= 1.4.12'
|
|
57
|
-
spec.add_dependency 'legion-data', '>= 1.5.
|
|
57
|
+
spec.add_dependency 'legion-data', '>= 1.5.3'
|
|
58
58
|
spec.add_dependency 'legion-json', '>= 1.2.1'
|
|
59
59
|
spec.add_dependency 'legion-logging', '>= 1.3.2'
|
|
60
60
|
spec.add_dependency 'legion-settings', '>= 1.3.19'
|
|
61
61
|
spec.add_dependency 'legion-transport', '>= 1.4.0'
|
|
62
62
|
|
|
63
|
-
spec.add_dependency 'legion-
|
|
63
|
+
spec.add_dependency 'legion-gaia', '>= 0.9.24'
|
|
64
|
+
spec.add_dependency 'legion-llm', '>= 0.5.8'
|
|
65
|
+
spec.add_dependency 'legion-tty', '>= 0.4.35'
|
|
64
66
|
spec.add_dependency 'lex-node'
|
|
65
67
|
end
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'socket'
|
|
6
|
+
require 'thor'
|
|
7
|
+
|
|
8
|
+
module Legion
|
|
9
|
+
module CLI
|
|
10
|
+
class Debug < Thor
|
|
11
|
+
namespace 'debug'
|
|
12
|
+
|
|
13
|
+
def self.exit_on_failure?
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class_option :json, type: :boolean, default: false, desc: 'Output as JSON'
|
|
18
|
+
class_option :no_color, type: :boolean, default: false, desc: 'Disable color output'
|
|
19
|
+
class_option :port, type: :numeric, default: 4567, desc: 'API port'
|
|
20
|
+
class_option :host, type: :string, default: '127.0.0.1', desc: 'API host'
|
|
21
|
+
class_option :config_dir, type: :string, desc: 'Config directory path'
|
|
22
|
+
|
|
23
|
+
desc 'dump', 'Full diagnostic dump (markdown, suitable for piping to LLM)'
|
|
24
|
+
default_task :dump
|
|
25
|
+
def dump
|
|
26
|
+
sections = {}
|
|
27
|
+
|
|
28
|
+
sections[:versions] = section_versions
|
|
29
|
+
sections[:doctor] = section_doctor
|
|
30
|
+
sections[:config] = section_config
|
|
31
|
+
sections[:gems] = section_gems
|
|
32
|
+
sections[:extensions] = section_extensions
|
|
33
|
+
sections[:rbac] = section_rbac
|
|
34
|
+
sections[:llm] = section_llm
|
|
35
|
+
sections[:gaia] = section_gaia
|
|
36
|
+
sections[:transport] = section_transport
|
|
37
|
+
sections[:events] = section_events
|
|
38
|
+
sections[:apollo] = section_apollo
|
|
39
|
+
sections[:remote_redis] = section_remote_redis
|
|
40
|
+
sections[:local_redis] = section_local_redis
|
|
41
|
+
sections[:postgresql] = section_postgresql
|
|
42
|
+
sections[:rabbitmq] = section_rabbitmq
|
|
43
|
+
sections[:api_health] = section_api_health
|
|
44
|
+
|
|
45
|
+
if options[:json]
|
|
46
|
+
puts ::JSON.pretty_generate(sections)
|
|
47
|
+
else
|
|
48
|
+
render_markdown(sections)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
no_commands do # rubocop:disable Metrics/BlockLength
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def api_host
|
|
56
|
+
options[:host] || '127.0.0.1'
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def api_port_number
|
|
60
|
+
options[:port] || 4567
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def api_get(path)
|
|
64
|
+
uri = URI("http://#{api_host}:#{api_port_number}#{path}")
|
|
65
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
66
|
+
http.open_timeout = 3
|
|
67
|
+
http.read_timeout = 5
|
|
68
|
+
response = http.get(uri.request_uri)
|
|
69
|
+
::JSON.parse(response.body, symbolize_names: true)
|
|
70
|
+
rescue StandardError => e
|
|
71
|
+
{ error: e.message }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def load_settings
|
|
75
|
+
Connection.config_dir = options[:config_dir] if options[:config_dir]
|
|
76
|
+
Connection.log_level = 'error'
|
|
77
|
+
Connection.ensure_settings
|
|
78
|
+
rescue StandardError
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def section_versions
|
|
83
|
+
components = {}
|
|
84
|
+
components[:legionio] = defined?(Legion::VERSION) ? Legion::VERSION : 'unknown'
|
|
85
|
+
components[:ruby] = RUBY_VERSION
|
|
86
|
+
components[:platform] = RUBY_PLATFORM
|
|
87
|
+
components[:yjit] = defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled?
|
|
88
|
+
|
|
89
|
+
%w[legion-transport legion-cache legion-crypt legion-data
|
|
90
|
+
legion-json legion-logging legion-settings
|
|
91
|
+
legion-llm legion-gaia legion-mcp legion-rbac legion-tty].each do |gem_name|
|
|
92
|
+
spec = Gem::Specification.find_by_name(gem_name)
|
|
93
|
+
components[gem_name.to_sym] = spec.version.to_s
|
|
94
|
+
rescue Gem::MissingSpecError
|
|
95
|
+
components[gem_name.to_sym] = 'not installed'
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
components
|
|
99
|
+
rescue StandardError => e
|
|
100
|
+
{ error: e.message }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def section_doctor
|
|
104
|
+
load_settings
|
|
105
|
+
require 'legion/cli/doctor_command'
|
|
106
|
+
Doctor::CHECKS.map do |name|
|
|
107
|
+
check = Doctor.const_get(name).new
|
|
108
|
+
result = check.run
|
|
109
|
+
{ name: result.name, status: result.status, message: result.message }
|
|
110
|
+
rescue StandardError => e
|
|
111
|
+
{ name: name.to_s, status: :error, message: e.message }
|
|
112
|
+
end
|
|
113
|
+
rescue StandardError => e
|
|
114
|
+
{ error: e.message }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def section_config
|
|
118
|
+
load_settings
|
|
119
|
+
settings_hash = Legion::Settings.loader.to_hash
|
|
120
|
+
redact_deep(settings_hash)
|
|
121
|
+
rescue StandardError => e
|
|
122
|
+
{ error: e.message }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def section_gems
|
|
126
|
+
gems = {}
|
|
127
|
+
duplicates = []
|
|
128
|
+
Gem::Specification.each do |spec|
|
|
129
|
+
next unless spec.name.start_with?('legion-', 'lex-', 'legionio')
|
|
130
|
+
|
|
131
|
+
gems[spec.name] ||= []
|
|
132
|
+
gems[spec.name] << spec.version.to_s
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
gems.each do |name, versions|
|
|
136
|
+
duplicates << { name: name, versions: versions } if versions.size > 1
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
{ total: gems.size, duplicates: duplicates,
|
|
140
|
+
versions: gems.transform_values { |v| v.max_by { |ver| Gem::Version.new(ver) } } }
|
|
141
|
+
rescue StandardError => e
|
|
142
|
+
{ error: e.message }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def section_extensions
|
|
146
|
+
data = api_get('/api/extensions')
|
|
147
|
+
return data if data[:error]
|
|
148
|
+
|
|
149
|
+
exts = data[:data] || data[:extensions] || data
|
|
150
|
+
{ count: exts.is_a?(Array) ? exts.size : nil, extensions: exts }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def section_rbac
|
|
154
|
+
api_get('/api/rbac/roles')
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def section_llm
|
|
158
|
+
load_settings
|
|
159
|
+
require 'legion/llm'
|
|
160
|
+
Legion::Settings.merge_settings(:llm, Legion::LLM::Settings.default)
|
|
161
|
+
settings = Legion::LLM.settings
|
|
162
|
+
providers = settings[:providers] || {}
|
|
163
|
+
{
|
|
164
|
+
started: defined?(Legion::LLM) && Legion::LLM.started?,
|
|
165
|
+
default_provider: settings[:default_provider],
|
|
166
|
+
default_model: settings[:default_model],
|
|
167
|
+
providers: providers.map { |name, cfg| { name: name, enabled: cfg[:enabled] } }
|
|
168
|
+
}
|
|
169
|
+
rescue StandardError => e
|
|
170
|
+
{ error: e.message }
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def section_gaia
|
|
174
|
+
status = api_get('/api/gaia/status')
|
|
175
|
+
channels = api_get('/api/gaia/channels')
|
|
176
|
+
buffer = api_get('/api/gaia/buffer')
|
|
177
|
+
sessions = api_get('/api/gaia/sessions')
|
|
178
|
+
{ status: status[:data] || status, channels: channels[:data] || channels,
|
|
179
|
+
buffer: buffer[:data] || buffer, sessions: sessions[:data] || sessions }
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def section_transport
|
|
183
|
+
api_get('/api/transport/status')
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def section_events
|
|
187
|
+
api_get('/api/events/recent?count=20')
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def section_apollo
|
|
191
|
+
api_get('/api/apollo/stats')
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def section_remote_redis
|
|
195
|
+
load_settings
|
|
196
|
+
cache_cfg = Legion::Settings[:cache]
|
|
197
|
+
return { error: 'no cache config' } unless cache_cfg.is_a?(Hash) && cache_cfg[:servers]
|
|
198
|
+
|
|
199
|
+
server = cache_cfg[:servers].first
|
|
200
|
+
host, port = server.to_s.split(':')
|
|
201
|
+
password = cache_cfg[:password]
|
|
202
|
+
|
|
203
|
+
redis_info(host, port.to_i, password)
|
|
204
|
+
rescue StandardError => e
|
|
205
|
+
{ error: e.message }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def section_local_redis
|
|
209
|
+
load_settings
|
|
210
|
+
local_cfg = Legion::Settings[:cache_local]
|
|
211
|
+
return { error: 'no cache_local config' } unless local_cfg.is_a?(Hash) && local_cfg[:servers]
|
|
212
|
+
|
|
213
|
+
server = local_cfg[:servers].first
|
|
214
|
+
host, port = server.to_s.split(':')
|
|
215
|
+
password = local_cfg[:password]
|
|
216
|
+
|
|
217
|
+
redis_info(host, port.to_i, password)
|
|
218
|
+
rescue StandardError => e
|
|
219
|
+
{ error: e.message }
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def section_postgresql
|
|
223
|
+
load_settings
|
|
224
|
+
data_cfg = Legion::Settings[:data]
|
|
225
|
+
return { error: 'no data config' } unless data_cfg.is_a?(Hash) && data_cfg[:creds]
|
|
226
|
+
|
|
227
|
+
creds = data_cfg[:creds]
|
|
228
|
+
require 'pg'
|
|
229
|
+
conn = PG.connect(
|
|
230
|
+
host: creds[:host], port: creds[:port] || 5432,
|
|
231
|
+
dbname: creds[:database], user: creds[:user], password: creds[:password],
|
|
232
|
+
connect_timeout: 5
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
db_size = conn.exec_params(
|
|
236
|
+
'SELECT pg_size_pretty(pg_database_size(current_database())) AS size'
|
|
237
|
+
).first['size']
|
|
238
|
+
migration = conn.exec_params(
|
|
239
|
+
'SELECT version FROM schema_info ORDER BY version DESC LIMIT 1'
|
|
240
|
+
).first
|
|
241
|
+
migration_version = migration ? migration['version'] : 'unknown'
|
|
242
|
+
|
|
243
|
+
tables = conn.exec_params(<<~SQL).to_a
|
|
244
|
+
SELECT tablename AS name,
|
|
245
|
+
pg_size_pretty(pg_total_relation_size(quote_ident(tablename))) AS size,
|
|
246
|
+
(SELECT n_live_tup FROM pg_stat_user_tables WHERE relname = tablename) AS rows
|
|
247
|
+
FROM pg_tables WHERE schemaname = 'public'
|
|
248
|
+
ORDER BY pg_total_relation_size(quote_ident(tablename)) DESC LIMIT 20
|
|
249
|
+
SQL
|
|
250
|
+
|
|
251
|
+
conn.close
|
|
252
|
+
{ db_size: db_size, migration_version: migration_version, tables: tables }
|
|
253
|
+
rescue LoadError
|
|
254
|
+
{ error: 'pg gem not available' }
|
|
255
|
+
rescue StandardError => e
|
|
256
|
+
{ error: e.message }
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def section_rabbitmq
|
|
260
|
+
load_settings
|
|
261
|
+
transport_cfg = Legion::Settings[:transport] || {}
|
|
262
|
+
host = transport_cfg[:host] || 'localhost'
|
|
263
|
+
mgmt_port = transport_cfg[:management_port] || 15_672
|
|
264
|
+
user = transport_cfg[:user] || 'guest'
|
|
265
|
+
pass = transport_cfg[:password] || 'guest'
|
|
266
|
+
vhost = transport_cfg[:vhost] || '/'
|
|
267
|
+
|
|
268
|
+
uri = URI("http://#{host}:#{mgmt_port}/api/overview")
|
|
269
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
270
|
+
http.open_timeout = 3
|
|
271
|
+
http.read_timeout = 5
|
|
272
|
+
req = Net::HTTP::Get.new(uri)
|
|
273
|
+
req.basic_auth(user, pass)
|
|
274
|
+
resp = http.request(req)
|
|
275
|
+
overview = ::JSON.parse(resp.body, symbolize_names: true)
|
|
276
|
+
|
|
277
|
+
encoded_vhost = URI.encode_www_form_component(vhost)
|
|
278
|
+
queues_uri = URI("http://#{host}:#{mgmt_port}/api/queues/#{encoded_vhost}")
|
|
279
|
+
req2 = Net::HTTP::Get.new("#{queues_uri.path}?page=1&page_size=15&sort=messages&sort_reverse=true")
|
|
280
|
+
req2.basic_auth(user, pass)
|
|
281
|
+
resp2 = http.request(req2)
|
|
282
|
+
queues = ::JSON.parse(resp2.body, symbolize_names: true)
|
|
283
|
+
|
|
284
|
+
queue_list = queues.is_a?(Array) ? queues : (queues[:items] || [])
|
|
285
|
+
|
|
286
|
+
{
|
|
287
|
+
cluster_name: overview[:cluster_name],
|
|
288
|
+
rabbitmq_version: overview[:rabbitmq_version],
|
|
289
|
+
erlang_version: overview[:erlang_version],
|
|
290
|
+
message_stats: overview[:message_stats],
|
|
291
|
+
queue_totals: overview[:queue_totals],
|
|
292
|
+
object_totals: overview[:object_totals],
|
|
293
|
+
top_queues: queue_list.first(15).map do |q|
|
|
294
|
+
{ name: q[:name], messages: q[:messages], consumers: q[:consumers] }
|
|
295
|
+
end
|
|
296
|
+
}
|
|
297
|
+
rescue StandardError => e
|
|
298
|
+
{ error: e.message }
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def section_api_health
|
|
302
|
+
ready = api_get('/api/ready')
|
|
303
|
+
health = api_get('/api/health')
|
|
304
|
+
capacity = api_get('/api/capacity')
|
|
305
|
+
cost = api_get('/api/cost/summary')
|
|
306
|
+
{ ready: ready, health: health, capacity: capacity, cost: cost }
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def redis_info(host, port, password)
|
|
310
|
+
socket = TCPSocket.new(host, port)
|
|
311
|
+
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
|
312
|
+
|
|
313
|
+
if password && !password.empty?
|
|
314
|
+
socket.write("AUTH #{password}\r\n")
|
|
315
|
+
auth_resp = socket.gets
|
|
316
|
+
return { error: "AUTH failed: #{auth_resp&.strip}" } unless auth_resp&.start_with?('+OK')
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
info = redis_command(socket, 'INFO memory')
|
|
320
|
+
dbsize_raw = redis_command(socket, 'DBSIZE')
|
|
321
|
+
|
|
322
|
+
socket.close
|
|
323
|
+
|
|
324
|
+
memory_lines = info.lines.select { |l| l.include?(':') }.to_h { |l| l.strip.split(':', 2) }
|
|
325
|
+
dbsize = dbsize_raw.to_s.scan(/\d+/).first
|
|
326
|
+
|
|
327
|
+
{
|
|
328
|
+
used_memory_human: memory_lines['used_memory_human'],
|
|
329
|
+
used_memory_peak_human: memory_lines['used_memory_peak_human'],
|
|
330
|
+
maxmemory_human: memory_lines['maxmemory_human'],
|
|
331
|
+
mem_fragmentation_ratio: memory_lines['mem_fragmentation_ratio'],
|
|
332
|
+
dbsize: dbsize
|
|
333
|
+
}
|
|
334
|
+
rescue StandardError => e
|
|
335
|
+
{ error: e.message }
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def redis_command(socket, cmd)
|
|
339
|
+
parts = cmd.split
|
|
340
|
+
socket.write("*#{parts.size}\r\n")
|
|
341
|
+
parts.each { |p| socket.write("$#{p.bytesize}\r\n#{p}\r\n") }
|
|
342
|
+
|
|
343
|
+
first = socket.gets
|
|
344
|
+
return '' unless first
|
|
345
|
+
|
|
346
|
+
case first[0]
|
|
347
|
+
when '+', ':' then first[1..].strip
|
|
348
|
+
when '-' then "ERROR: #{first[1..].strip}"
|
|
349
|
+
when '$'
|
|
350
|
+
len = first[1..].to_i
|
|
351
|
+
return '' if len.negative?
|
|
352
|
+
|
|
353
|
+
data = socket.read(len + 2)
|
|
354
|
+
data&.strip || ''
|
|
355
|
+
when '*'
|
|
356
|
+
count = first[1..].to_i
|
|
357
|
+
return '' if count.negative?
|
|
358
|
+
|
|
359
|
+
count.times.map { redis_read_bulk(socket) }.join("\n")
|
|
360
|
+
else
|
|
361
|
+
first.strip
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def redis_read_bulk(socket)
|
|
366
|
+
header = socket.gets
|
|
367
|
+
return '' unless header&.start_with?('$')
|
|
368
|
+
|
|
369
|
+
len = header[1..].to_i
|
|
370
|
+
return '' if len.negative?
|
|
371
|
+
|
|
372
|
+
data = socket.read(len + 2)
|
|
373
|
+
data&.strip || ''
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def redact_deep(obj)
|
|
377
|
+
case obj
|
|
378
|
+
when Hash
|
|
379
|
+
obj.each_with_object({}) do |(k, v), h|
|
|
380
|
+
h[k] = if k.to_s.match?(/password|secret|token|key|credential/i) && v.is_a?(String)
|
|
381
|
+
'[REDACTED]'
|
|
382
|
+
else
|
|
383
|
+
redact_deep(v)
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
when Array
|
|
387
|
+
obj.map { |v| redact_deep(v) }
|
|
388
|
+
else
|
|
389
|
+
obj
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def render_markdown(sections)
|
|
394
|
+
puts '# LegionIO Diagnostic Dump'
|
|
395
|
+
puts
|
|
396
|
+
puts "Generated: #{Time.now.utc.iso8601}"
|
|
397
|
+
puts
|
|
398
|
+
|
|
399
|
+
md_section('Versions', sections[:versions])
|
|
400
|
+
md_section('Doctor Checks', sections[:doctor])
|
|
401
|
+
md_section('Configuration (redacted)', sections[:config])
|
|
402
|
+
md_section('Installed Gems', sections[:gems])
|
|
403
|
+
md_section('Loaded Extensions', sections[:extensions])
|
|
404
|
+
md_section('RBAC Roles', sections[:rbac])
|
|
405
|
+
md_section('LLM Status', sections[:llm])
|
|
406
|
+
md_section('GAIA Status', sections[:gaia])
|
|
407
|
+
md_section('Transport Status', sections[:transport])
|
|
408
|
+
md_section('Recent Events (last 20)', sections[:events])
|
|
409
|
+
md_section('Apollo Stats', sections[:apollo])
|
|
410
|
+
md_section('Remote Redis', sections[:remote_redis])
|
|
411
|
+
md_section('Local Redis', sections[:local_redis])
|
|
412
|
+
md_section('PostgreSQL', sections[:postgresql])
|
|
413
|
+
md_section('RabbitMQ', sections[:rabbitmq])
|
|
414
|
+
md_section('API Health', sections[:api_health])
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def md_section(title, data)
|
|
418
|
+
puts "## #{title}"
|
|
419
|
+
puts
|
|
420
|
+
puts '```json'
|
|
421
|
+
puts ::JSON.pretty_generate(data)
|
|
422
|
+
puts '```'
|
|
423
|
+
puts
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
end
|
|
@@ -6,6 +6,7 @@ require 'rbconfig'
|
|
|
6
6
|
require 'concurrent'
|
|
7
7
|
require 'net/http'
|
|
8
8
|
require 'json'
|
|
9
|
+
require 'rubygems/uninstaller'
|
|
9
10
|
|
|
10
11
|
module Legion
|
|
11
12
|
module CLI
|
|
@@ -22,6 +23,7 @@ module Legion
|
|
|
22
23
|
desc 'gems', 'Update Legion gems to latest versions (default)'
|
|
23
24
|
default_task :gems
|
|
24
25
|
option :dry_run, type: :boolean, default: false, desc: 'Show what would be updated without installing'
|
|
26
|
+
option :cleanup, type: :boolean, default: false, desc: 'Remove old gem versions after update'
|
|
25
27
|
def gems
|
|
26
28
|
out = formatter
|
|
27
29
|
gem_bin = File.join(RbConfig::CONFIG['bindir'], 'gem')
|
|
@@ -44,6 +46,8 @@ module Legion
|
|
|
44
46
|
else
|
|
45
47
|
display_results(out, results, before, after)
|
|
46
48
|
end
|
|
49
|
+
|
|
50
|
+
cleanup_old_gems(out, target_gems) if options[:cleanup] && !options[:dry_run]
|
|
47
51
|
end
|
|
48
52
|
|
|
49
53
|
no_commands do
|
|
@@ -66,11 +70,12 @@ module Legion
|
|
|
66
70
|
|
|
67
71
|
def snapshot_versions(gem_names)
|
|
68
72
|
gem_names.each_with_object({}) do |name, hash|
|
|
69
|
-
|
|
70
|
-
hash[name] =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
specs = Gem::Specification.find_all_by_name(name)
|
|
74
|
+
hash[name] = if specs.empty?
|
|
75
|
+
nil
|
|
76
|
+
else
|
|
77
|
+
specs.map(&:version).max.to_s
|
|
78
|
+
end
|
|
74
79
|
end
|
|
75
80
|
end
|
|
76
81
|
|
|
@@ -174,6 +179,39 @@ module Legion
|
|
|
174
179
|
suggest_detect(out)
|
|
175
180
|
end
|
|
176
181
|
|
|
182
|
+
def cleanup_old_gems(out, gem_names)
|
|
183
|
+
Gem::Specification.reset
|
|
184
|
+
cleaned = 0
|
|
185
|
+
|
|
186
|
+
gem_names.each do |name|
|
|
187
|
+
specs = Gem::Specification.find_all_by_name(name).sort_by(&:version)
|
|
188
|
+
next if specs.size <= 1
|
|
189
|
+
|
|
190
|
+
latest = specs.pop
|
|
191
|
+
specs.each do |old_spec|
|
|
192
|
+
Gem::Uninstaller.new(
|
|
193
|
+
old_spec.name,
|
|
194
|
+
version: old_spec.version,
|
|
195
|
+
ignore: true,
|
|
196
|
+
executables: false,
|
|
197
|
+
force: true,
|
|
198
|
+
abort_on_dependent: false
|
|
199
|
+
).uninstall
|
|
200
|
+
out.success(" Cleaned #{old_spec.name}-#{old_spec.version} (keeping #{latest.version})")
|
|
201
|
+
cleaned += 1
|
|
202
|
+
rescue StandardError => e
|
|
203
|
+
out.error(" Failed to clean #{old_spec.name}-#{old_spec.version}: #{e.message}")
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
out.spacer
|
|
208
|
+
if cleaned.positive?
|
|
209
|
+
out.success("Cleaned #{cleaned} old gem version(s)")
|
|
210
|
+
else
|
|
211
|
+
puts 'No old gem versions to clean'
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
177
215
|
def suggest_detect(out)
|
|
178
216
|
require 'legion/extensions/detect'
|
|
179
217
|
missing = Legion::Extensions::Detect.missing
|
data/lib/legion/cli.rb
CHANGED
|
@@ -62,6 +62,7 @@ module Legion
|
|
|
62
62
|
autoload :Apollo, 'legion/cli/apollo_command'
|
|
63
63
|
autoload :TraceCommand, 'legion/cli/trace_command'
|
|
64
64
|
autoload :Features, 'legion/cli/features_command'
|
|
65
|
+
autoload :Debug, 'legion/cli/debug_command'
|
|
65
66
|
|
|
66
67
|
class Main < Thor
|
|
67
68
|
def self.exit_on_failure?
|
|
@@ -342,6 +343,9 @@ module Legion
|
|
|
342
343
|
desc 'features SUBCOMMAND', 'Install feature bundles (interactive selector)'
|
|
343
344
|
subcommand 'features', Legion::CLI::Features
|
|
344
345
|
|
|
346
|
+
desc 'debug', 'Diagnostic dump for troubleshooting (pipe to LLM for analysis)'
|
|
347
|
+
subcommand 'debug', Legion::CLI::Debug
|
|
348
|
+
|
|
345
349
|
desc 'tree', 'Print a tree of all available commands'
|
|
346
350
|
def tree
|
|
347
351
|
legion_print_command_tree(self.class, 'legion', '')
|
data/lib/legion/service.rb
CHANGED
|
@@ -236,6 +236,11 @@ module Legion
|
|
|
236
236
|
end
|
|
237
237
|
|
|
238
238
|
def setup_api
|
|
239
|
+
if @api_thread&.alive?
|
|
240
|
+
Legion::Logging.warn 'API already running, skipping duplicate setup_api call'
|
|
241
|
+
return
|
|
242
|
+
end
|
|
243
|
+
|
|
239
244
|
require 'legion/api'
|
|
240
245
|
api_settings = Legion::Settings[:api] || {}
|
|
241
246
|
port = api_settings[:port] || 4567
|
data/lib/legion/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legionio
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.5.
|
|
4
|
+
version: 1.5.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -253,14 +253,14 @@ dependencies:
|
|
|
253
253
|
requirements:
|
|
254
254
|
- - ">="
|
|
255
255
|
- !ruby/object:Gem::Version
|
|
256
|
-
version: 1.5.
|
|
256
|
+
version: 1.5.3
|
|
257
257
|
type: :runtime
|
|
258
258
|
prerelease: false
|
|
259
259
|
version_requirements: !ruby/object:Gem::Requirement
|
|
260
260
|
requirements:
|
|
261
261
|
- - ">="
|
|
262
262
|
- !ruby/object:Gem::Version
|
|
263
|
-
version: 1.5.
|
|
263
|
+
version: 1.5.3
|
|
264
264
|
- !ruby/object:Gem::Dependency
|
|
265
265
|
name: legion-json
|
|
266
266
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -317,20 +317,48 @@ dependencies:
|
|
|
317
317
|
- - ">="
|
|
318
318
|
- !ruby/object:Gem::Version
|
|
319
319
|
version: 1.4.0
|
|
320
|
+
- !ruby/object:Gem::Dependency
|
|
321
|
+
name: legion-gaia
|
|
322
|
+
requirement: !ruby/object:Gem::Requirement
|
|
323
|
+
requirements:
|
|
324
|
+
- - ">="
|
|
325
|
+
- !ruby/object:Gem::Version
|
|
326
|
+
version: 0.9.24
|
|
327
|
+
type: :runtime
|
|
328
|
+
prerelease: false
|
|
329
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
330
|
+
requirements:
|
|
331
|
+
- - ">="
|
|
332
|
+
- !ruby/object:Gem::Version
|
|
333
|
+
version: 0.9.24
|
|
334
|
+
- !ruby/object:Gem::Dependency
|
|
335
|
+
name: legion-llm
|
|
336
|
+
requirement: !ruby/object:Gem::Requirement
|
|
337
|
+
requirements:
|
|
338
|
+
- - ">="
|
|
339
|
+
- !ruby/object:Gem::Version
|
|
340
|
+
version: 0.5.8
|
|
341
|
+
type: :runtime
|
|
342
|
+
prerelease: false
|
|
343
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
344
|
+
requirements:
|
|
345
|
+
- - ">="
|
|
346
|
+
- !ruby/object:Gem::Version
|
|
347
|
+
version: 0.5.8
|
|
320
348
|
- !ruby/object:Gem::Dependency
|
|
321
349
|
name: legion-tty
|
|
322
350
|
requirement: !ruby/object:Gem::Requirement
|
|
323
351
|
requirements:
|
|
324
352
|
- - ">="
|
|
325
353
|
- !ruby/object:Gem::Version
|
|
326
|
-
version: 0.4.
|
|
354
|
+
version: 0.4.35
|
|
327
355
|
type: :runtime
|
|
328
356
|
prerelease: false
|
|
329
357
|
version_requirements: !ruby/object:Gem::Requirement
|
|
330
358
|
requirements:
|
|
331
359
|
- - ">="
|
|
332
360
|
- !ruby/object:Gem::Version
|
|
333
|
-
version: 0.4.
|
|
361
|
+
version: 0.4.35
|
|
334
362
|
- !ruby/object:Gem::Dependency
|
|
335
363
|
name: lex-node
|
|
336
364
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -555,6 +583,7 @@ files:
|
|
|
555
583
|
- lib/legion/cli/dashboard/renderer.rb
|
|
556
584
|
- lib/legion/cli/dashboard_command.rb
|
|
557
585
|
- lib/legion/cli/dataset_command.rb
|
|
586
|
+
- lib/legion/cli/debug_command.rb
|
|
558
587
|
- lib/legion/cli/detect_command.rb
|
|
559
588
|
- lib/legion/cli/do_command.rb
|
|
560
589
|
- lib/legion/cli/docs_command.rb
|