legionio 1.7.8 → 1.7.12
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/.rubocop.yml +3 -1
- data/CHANGELOG.md +20 -0
- data/legionio.gemspec +8 -8
- data/lib/legion/api/audit.rb +1 -1
- data/lib/legion/api/events.rb +38 -15
- data/lib/legion/api/helpers.rb +29 -7
- data/lib/legion/api/library_routes.rb +3 -0
- data/lib/legion/api/tenants.rb +5 -6
- data/lib/legion/api/workers.rb +1 -14
- data/lib/legion/api.rb +21 -2
- data/lib/legion/cli/chat/chat_logger.rb +19 -12
- data/lib/legion/cli/config_command.rb +1 -1
- data/lib/legion/cli/error_handler.rb +8 -2
- data/lib/legion/cli/start.rb +3 -2
- data/lib/legion/cli.rb +13 -2
- data/lib/legion/extensions/catalog.rb +77 -11
- data/lib/legion/extensions/core.rb +23 -6
- data/lib/legion/extensions/helpers/secret.rb +2 -0
- data/lib/legion/extensions/transport.rb +15 -2
- data/lib/legion/region.rb +36 -1
- data/lib/legion/service.rb +174 -127
- data/lib/legion/task_outcome_observer.rb +32 -8
- data/lib/legion/telemetry.rb +19 -11
- data/lib/legion/version.rb +1 -1
- data/lib/legion/webhooks.rb +169 -40
- metadata +17 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 60c67de284b066264f07c3ad829b8d193b66d85ae900af468dd7a13165695da8
|
|
4
|
+
data.tar.gz: 8fd7ab97436343722619c6c77e0aaff9b6f664609db1b81a0daff97c6692f419
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2fde83c468f409a35fc9e02d1dc39d969a0229d32c8b592162789683f21afa31df90763792a1c4c74cb74f13ef21f1288e125d78b2b68ac2f9d724a08eee1e77
|
|
7
|
+
data.tar.gz: d10be584f4e30e1f164545c5c6e72f2e3cfa627e19675021edaf18ee6224d3f66d92ce50fcaf44b40caf0e47515e61d137e921f3903e5cab34254245fcd84bfa
|
data/.rubocop.yml
CHANGED
|
@@ -5,6 +5,8 @@ AllCops:
|
|
|
5
5
|
|
|
6
6
|
Layout/LineLength:
|
|
7
7
|
Max: 160
|
|
8
|
+
Exclude:
|
|
9
|
+
- 'Gemfile'
|
|
8
10
|
|
|
9
11
|
Layout/SpaceAroundEqualsInParameterDefault:
|
|
10
12
|
EnforcedStyle: space
|
|
@@ -65,7 +67,7 @@ Metrics/BlockLength:
|
|
|
65
67
|
- 'lib/legion/cli/mode_command.rb'
|
|
66
68
|
|
|
67
69
|
Metrics/AbcSize:
|
|
68
|
-
Max:
|
|
70
|
+
Max: 62
|
|
69
71
|
Exclude:
|
|
70
72
|
- 'lib/legion/cli/chat_command.rb'
|
|
71
73
|
- 'lib/legion/api/llm.rb'
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.7.12] - 2026-04-03
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Fixes #110: normal daemon boot now prefers library-owned LLM and Apollo API routes, `/api/tenants` uses canonical JSON parsing with correct status codes, SSE listeners drain worker threads on disconnect, paginated collections avoid unconditional `COUNT(*)` unless explicitly requested, and service startup skips duplicate settings loads once configuration is already bootstrapped
|
|
9
|
+
|
|
10
|
+
## [1.7.11] - 2026-04-02
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Fixes #113: webhook deliveries now retry non-2xx responses and transport exceptions up to `max_retries`, record per-attempt delivery rows, dead-letter terminal failures, and cache active webhook pattern matching to reduce per-event dispatch overhead
|
|
14
|
+
|
|
15
|
+
## [1.7.10] - 2026-04-02
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Bumped minimum dependency floors for Legion core gems, including `legion-logging >= 1.5.0`, `legion-settings >= 1.3.25`, and updated transport, data, cache, crypt, Apollo, and MCP minimums
|
|
19
|
+
- Stabilized the `LegionIO` spec suite by fixing the OAuth callback, catalog, and service shutdown regression specs
|
|
20
|
+
- CLI startup now honors settings-driven log levels, normalizes `start --help` into the standard Thor help flow, and routes chat/error logging through the newer helper-backed logger path
|
|
21
|
+
- `Legion::Service`, telemetry, and webhook runtime paths now use structured helper logging more consistently, respect configured logging when no CLI override is passed, and avoid brittle settings reads during boot
|
|
22
|
+
- Extension runtime wiring now deep-dups merged settings, lazily registers the local `extension_catalog` migration, publishes catalog transitions directly to transport, and surfaces auto-binding failures more clearly
|
|
23
|
+
- Secret, region, and task-outcome helpers now use canonical Vault connectivity checks, cache metadata misses more safely, and create meta-learning domains on demand before recording learning episodes
|
|
24
|
+
|
|
5
25
|
## [1.7.8] - 2026-04-01
|
|
6
26
|
|
|
7
27
|
### Added
|
data/legionio.gemspec
CHANGED
|
@@ -34,7 +34,7 @@ Gem::Specification.new do |spec|
|
|
|
34
34
|
spec.bindir = 'exe'
|
|
35
35
|
spec.executables = %w[legion legionio]
|
|
36
36
|
|
|
37
|
-
spec.add_dependency 'legion-mcp', '>= 0.
|
|
37
|
+
spec.add_dependency 'legion-mcp', '>= 0.7.1'
|
|
38
38
|
|
|
39
39
|
spec.add_dependency 'kramdown', '>= 2.0'
|
|
40
40
|
|
|
@@ -52,15 +52,15 @@ Gem::Specification.new do |spec|
|
|
|
52
52
|
spec.add_dependency 'thor', '>= 1.3'
|
|
53
53
|
spec.add_dependency 'tty-spinner', '~> 0.9'
|
|
54
54
|
|
|
55
|
-
spec.add_dependency 'legion-cache', '>= 1.3.
|
|
56
|
-
spec.add_dependency 'legion-crypt', '>= 1.
|
|
57
|
-
spec.add_dependency 'legion-data', '>= 1.6.
|
|
55
|
+
spec.add_dependency 'legion-cache', '>= 1.3.21'
|
|
56
|
+
spec.add_dependency 'legion-crypt', '>= 1.5.0'
|
|
57
|
+
spec.add_dependency 'legion-data', '>= 1.6.19'
|
|
58
58
|
spec.add_dependency 'legion-json', '>= 1.2.1'
|
|
59
|
-
spec.add_dependency 'legion-logging', '>= 1.
|
|
60
|
-
spec.add_dependency 'legion-settings', '>= 1.3.
|
|
61
|
-
spec.add_dependency 'legion-transport', '>= 1.4.
|
|
59
|
+
spec.add_dependency 'legion-logging', '>= 1.5.0'
|
|
60
|
+
spec.add_dependency 'legion-settings', '>= 1.3.25'
|
|
61
|
+
spec.add_dependency 'legion-transport', '>= 1.4.13'
|
|
62
62
|
|
|
63
|
-
spec.add_dependency 'legion-apollo', '>= 0.
|
|
63
|
+
spec.add_dependency 'legion-apollo', '>= 0.4.0'
|
|
64
64
|
spec.add_dependency 'legion-gaia', '>= 0.9.26'
|
|
65
65
|
spec.add_dependency 'legion-llm', '>= 0.5.8'
|
|
66
66
|
spec.add_dependency 'legion-tty', '>= 0.4.35'
|
data/lib/legion/api/audit.rb
CHANGED
|
@@ -4,7 +4,7 @@ module Legion
|
|
|
4
4
|
class API < Sinatra::Base
|
|
5
5
|
module Routes
|
|
6
6
|
module Audit
|
|
7
|
-
def self.registered(app)
|
|
7
|
+
def self.registered(app)
|
|
8
8
|
app.get '/api/audit' do
|
|
9
9
|
require_data!
|
|
10
10
|
dataset = Legion::Data::Model::AuditLog.order(Sequel.desc(:id))
|
data/lib/legion/api/events.rb
CHANGED
|
@@ -5,6 +5,7 @@ module Legion
|
|
|
5
5
|
module Routes
|
|
6
6
|
module Events
|
|
7
7
|
BUFFER_SIZE = 100
|
|
8
|
+
SSE_STOP = Object.new.freeze
|
|
8
9
|
|
|
9
10
|
class << self
|
|
10
11
|
def event_buffer
|
|
@@ -38,6 +39,42 @@ module Legion
|
|
|
38
39
|
@listener_installed = true
|
|
39
40
|
end
|
|
40
41
|
|
|
42
|
+
def write_sse_event(out, event)
|
|
43
|
+
payload = event.transform_keys(&:to_s)
|
|
44
|
+
out << "event: #{payload['event']}\ndata: #{Legion::JSON.dump(payload)}\n\n"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def stop_queue_stream(queue:, worker:, listener:)
|
|
48
|
+
Legion::Events.off('*', listener) if defined?(Legion::Events)
|
|
49
|
+
return unless worker&.alive?
|
|
50
|
+
|
|
51
|
+
queue.push(SSE_STOP)
|
|
52
|
+
worker.join(0.1)
|
|
53
|
+
rescue ThreadError, IOError, Errno::EPIPE => e
|
|
54
|
+
Legion::Logging.debug("Events SSE cleanup failed: #{e.message}") if defined?(Legion::Logging)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def stream_queue(out:, queue:, listener:)
|
|
58
|
+
worker = Thread.new do
|
|
59
|
+
loop do
|
|
60
|
+
event = queue.pop
|
|
61
|
+
break if event.equal?(SSE_STOP)
|
|
62
|
+
|
|
63
|
+
write_sse_event(out, event)
|
|
64
|
+
rescue IOError, Errno::EPIPE => e
|
|
65
|
+
Legion::Logging.debug("Events SSE stream broken for #{event[:event]}: #{e.message}") if defined?(Legion::Logging)
|
|
66
|
+
break
|
|
67
|
+
end
|
|
68
|
+
ensure
|
|
69
|
+
Legion::Events.off('*', listener) if defined?(Legion::Events)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
cleanup = proc { stop_queue_stream(queue: queue, worker: worker, listener: listener) }
|
|
73
|
+
out.callback(&cleanup)
|
|
74
|
+
out.errback(&cleanup)
|
|
75
|
+
worker
|
|
76
|
+
end
|
|
77
|
+
|
|
41
78
|
def registered(app)
|
|
42
79
|
install_listener if defined?(Legion::Events)
|
|
43
80
|
|
|
@@ -53,21 +90,7 @@ module Legion
|
|
|
53
90
|
end
|
|
54
91
|
|
|
55
92
|
stream do |out|
|
|
56
|
-
|
|
57
|
-
loop do
|
|
58
|
-
event = queue.pop
|
|
59
|
-
data = Legion::JSON.dump(event.transform_keys(&:to_s))
|
|
60
|
-
out << "event: #{event[:event]}\ndata: #{data}\n\n"
|
|
61
|
-
rescue IOError, Errno::EPIPE => e
|
|
62
|
-
Legion::Logging.debug "Events SSE stream broken for #{event[:event]}: #{e.message}" if defined?(Legion::Logging)
|
|
63
|
-
break
|
|
64
|
-
end
|
|
65
|
-
ensure
|
|
66
|
-
Legion::Events.off('*', listener)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
out.callback { Legion::Events.off('*', listener) }
|
|
70
|
-
out.errback { Legion::Events.off('*', listener) }
|
|
93
|
+
stream_queue(out: out, queue: queue, listener: listener)
|
|
71
94
|
end
|
|
72
95
|
end
|
|
73
96
|
|
data/lib/legion/api/helpers.rb
CHANGED
|
@@ -16,17 +16,20 @@ module Legion
|
|
|
16
16
|
content_type :json
|
|
17
17
|
status status_code
|
|
18
18
|
|
|
19
|
-
total = dataset.respond_to?(:count) ? dataset.count : dataset.length
|
|
20
19
|
paginated = paginate(dataset)
|
|
21
|
-
items = paginated.respond_to?(:all) ? paginated.all : paginated
|
|
20
|
+
items = paginated.respond_to?(:all) ? paginated.all : Array(paginated)
|
|
21
|
+
total = collection_total(dataset, items)
|
|
22
|
+
meta = response_meta.merge(
|
|
23
|
+
count: items.length,
|
|
24
|
+
limit: page_limit,
|
|
25
|
+
offset: page_offset
|
|
26
|
+
)
|
|
27
|
+
meta[:total] = total unless total.nil?
|
|
28
|
+
meta[:has_more] = collection_has_more?(items, total)
|
|
22
29
|
|
|
23
30
|
Legion::JSON.dump({
|
|
24
31
|
data: items.map { |r| r.respond_to?(:values) ? r.values : r },
|
|
25
|
-
meta:
|
|
26
|
-
total: total,
|
|
27
|
-
limit: page_limit,
|
|
28
|
-
offset: page_offset
|
|
29
|
-
)
|
|
32
|
+
meta: meta
|
|
30
33
|
})
|
|
31
34
|
end
|
|
32
35
|
|
|
@@ -198,6 +201,25 @@ module Legion
|
|
|
198
201
|
dataset
|
|
199
202
|
end
|
|
200
203
|
end
|
|
204
|
+
|
|
205
|
+
def include_total_count?
|
|
206
|
+
params[:include_total].to_s == 'true'
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def collection_total(dataset, items)
|
|
210
|
+
return dataset.count if include_total_count? && dataset.respond_to?(:count)
|
|
211
|
+
return dataset.length if dataset.respond_to?(:length) && !dataset.respond_to?(:limit)
|
|
212
|
+
|
|
213
|
+
return page_offset + items.length if items.length < page_limit
|
|
214
|
+
|
|
215
|
+
nil
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def collection_has_more?(items, total)
|
|
219
|
+
return (page_offset + items.length) < total if total
|
|
220
|
+
|
|
221
|
+
items.length == page_limit
|
|
222
|
+
end
|
|
201
223
|
end
|
|
202
224
|
end
|
|
203
225
|
end
|
|
@@ -11,6 +11,9 @@ module Legion
|
|
|
11
11
|
# @param gem_name [String] short name for the library (e.g. 'llm', 'apollo')
|
|
12
12
|
# @param routes_module [Module] a Sinatra::Extension module to register
|
|
13
13
|
def self.register_library_routes(gem_name, routes_module)
|
|
14
|
+
existing = router.library_routes[gem_name.to_s]
|
|
15
|
+
return routes_module if existing == routes_module
|
|
16
|
+
|
|
14
17
|
router.register_library(gem_name, routes_module)
|
|
15
18
|
register routes_module
|
|
16
19
|
end
|
data/lib/legion/api/tenants.rb
CHANGED
|
@@ -13,14 +13,13 @@ module Legion
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
app.post '/api/tenants' do
|
|
16
|
-
|
|
16
|
+
body = parse_request_body
|
|
17
17
|
result = Legion::Tenants.create(
|
|
18
|
-
tenant_id:
|
|
19
|
-
name:
|
|
20
|
-
max_workers:
|
|
18
|
+
tenant_id: body[:tenant_id],
|
|
19
|
+
name: body[:name],
|
|
20
|
+
max_workers: body[:max_workers] || 10
|
|
21
21
|
)
|
|
22
|
-
|
|
23
|
-
json_response(data: result)
|
|
22
|
+
json_response(result, status_code: result[:error] ? 409 : 201)
|
|
24
23
|
end
|
|
25
24
|
|
|
26
25
|
app.get '/api/tenants/:tenant_id' do
|
data/lib/legion/api/workers.rb
CHANGED
|
@@ -167,20 +167,7 @@ module Legion
|
|
|
167
167
|
end
|
|
168
168
|
|
|
169
169
|
stream do |out|
|
|
170
|
-
|
|
171
|
-
loop do
|
|
172
|
-
event = queue.pop
|
|
173
|
-
data = Legion::JSON.dump({ **event.transform_keys(&:to_s) })
|
|
174
|
-
out << "event: #{event[:event]}\ndata: #{data}\n\n"
|
|
175
|
-
rescue IOError, Errno::EPIPE
|
|
176
|
-
break
|
|
177
|
-
end
|
|
178
|
-
ensure
|
|
179
|
-
Legion::Events.off('*', listener)
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
out.callback { Legion::Events.off('*', listener) }
|
|
183
|
-
out.errback { Legion::Events.off('*', listener) }
|
|
170
|
+
Routes::Events.stream_queue(out: out, queue: queue, listener: listener)
|
|
184
171
|
end
|
|
185
172
|
else
|
|
186
173
|
count = (params[:count] || 25).to_i
|
data/lib/legion/api.rb
CHANGED
|
@@ -149,6 +149,25 @@ module Legion
|
|
|
149
149
|
def router
|
|
150
150
|
@router ||= Legion::API::Router.new
|
|
151
151
|
end
|
|
152
|
+
|
|
153
|
+
def mount_library_routes(gem_name, fallback_module, preferred_constant_path)
|
|
154
|
+
preferred = constant_from_path(preferred_constant_path)
|
|
155
|
+
if preferred.is_a?(Module)
|
|
156
|
+
register_library_routes(gem_name, preferred)
|
|
157
|
+
elsif router.library_names.include?(gem_name)
|
|
158
|
+
register_library_routes(gem_name, router.library_routes.fetch(gem_name))
|
|
159
|
+
else
|
|
160
|
+
register fallback_module
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
private
|
|
165
|
+
|
|
166
|
+
def constant_from_path(path)
|
|
167
|
+
path.to_s.split('::').reject(&:empty?).reduce(Object) { |scope, name| scope.const_get(name) }
|
|
168
|
+
rescue NameError
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
152
171
|
end
|
|
153
172
|
|
|
154
173
|
# Mount route modules
|
|
@@ -175,14 +194,14 @@ module Legion
|
|
|
175
194
|
register Routes::Capacity
|
|
176
195
|
register Routes::Audit
|
|
177
196
|
register Routes::Metrics
|
|
178
|
-
|
|
197
|
+
mount_library_routes('llm', Routes::Llm, 'Legion::LLM::Routes')
|
|
179
198
|
register Routes::ExtensionCatalog
|
|
180
199
|
register Routes::OrgChart
|
|
181
200
|
register Routes::Governance
|
|
182
201
|
register Routes::Acp
|
|
183
202
|
register Routes::Prompts
|
|
184
203
|
register Routes::Marketplace
|
|
185
|
-
|
|
204
|
+
mount_library_routes('apollo', Routes::Apollo, 'Legion::Apollo::Routes')
|
|
186
205
|
register Routes::Costs
|
|
187
206
|
register Routes::Traces
|
|
188
207
|
register Routes::Stats
|
|
@@ -8,8 +8,14 @@ module Legion
|
|
|
8
8
|
module CLI
|
|
9
9
|
class Chat
|
|
10
10
|
module ChatLogger
|
|
11
|
-
LOG_DIR
|
|
11
|
+
LOG_DIR = File.expand_path('~/.legion')
|
|
12
12
|
LOG_FILE = File.join(LOG_DIR, 'legion-chat.log')
|
|
13
|
+
LEVELS = {
|
|
14
|
+
'debug' => ::Logger::DEBUG,
|
|
15
|
+
'info' => ::Logger::INFO,
|
|
16
|
+
'warn' => ::Logger::WARN,
|
|
17
|
+
'error' => ::Logger::ERROR
|
|
18
|
+
}.freeze
|
|
13
19
|
|
|
14
20
|
class << self
|
|
15
21
|
attr_reader :logger
|
|
@@ -22,20 +28,21 @@ module Legion
|
|
|
22
28
|
@logger
|
|
23
29
|
end
|
|
24
30
|
|
|
25
|
-
def debug(msg)
|
|
26
|
-
|
|
27
|
-
def
|
|
28
|
-
|
|
31
|
+
def debug(msg) = logger&.debug(msg)
|
|
32
|
+
|
|
33
|
+
def info(msg) = logger&.info(msg)
|
|
34
|
+
|
|
35
|
+
def warn(msg) = logger&.warn(msg)
|
|
36
|
+
|
|
37
|
+
def error(msg) = logger&.error(msg)
|
|
29
38
|
|
|
30
39
|
private
|
|
31
40
|
|
|
32
|
-
def parse_level(level)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
else ::Logger::INFO
|
|
38
|
-
end
|
|
41
|
+
def parse_level(level = 'info')
|
|
42
|
+
normalized_level = level.to_s.strip.downcase
|
|
43
|
+
return ::Logger::INFO if normalized_level.empty?
|
|
44
|
+
|
|
45
|
+
LEVELS.fetch(normalized_level, ::Logger::INFO)
|
|
39
46
|
end
|
|
40
47
|
|
|
41
48
|
def format_entry(severity, datetime, _progname, msg)
|
|
@@ -101,7 +101,7 @@ module Legion
|
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
desc 'validate', 'Validate current configuration'
|
|
104
|
-
def validate # rubocop:disable Metrics/
|
|
104
|
+
def validate # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
105
105
|
out = formatter
|
|
106
106
|
Connection.config_dir = options[:config_dir] if options[:config_dir]
|
|
107
107
|
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module CLI
|
|
5
7
|
module ErrorHandler
|
|
8
|
+
extend Legion::Logging::Helper
|
|
9
|
+
|
|
6
10
|
PATTERNS = [
|
|
7
11
|
{
|
|
8
12
|
match: /connection refused.*5672|ECONNREFUSED.*5672|bunny.*not connected/i,
|
|
@@ -71,11 +75,13 @@ module Legion
|
|
|
71
75
|
def wrap(error)
|
|
72
76
|
pattern = PATTERNS.find { |p| error.message.match?(p[:match]) }
|
|
73
77
|
unless pattern
|
|
74
|
-
|
|
78
|
+
handle_exception(error, level: :error, handled: true, operation: :wrap_cli_error, matched: false) if logging_available?
|
|
79
|
+
log.error("[CLI] unhandled error: #{error.class} - #{error.message}") if logging_available?
|
|
75
80
|
return error
|
|
76
81
|
end
|
|
77
82
|
|
|
78
|
-
|
|
83
|
+
handle_exception(error, level: :warn, handled: true, operation: :wrap_cli_error, code: pattern[:code]) if logging_available?
|
|
84
|
+
log.warn("[CLI] matched error pattern :#{pattern[:code]} - #{error.message}") if logging_available?
|
|
79
85
|
Error.actionable(
|
|
80
86
|
code: pattern[:code],
|
|
81
87
|
message: "#{pattern[:message]}: #{error.message}",
|
data/lib/legion/cli/start.rb
CHANGED
|
@@ -10,7 +10,7 @@ module Legion
|
|
|
10
10
|
ENV['LEGION_LOCAL'] = 'true'
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
log_level = options[:log_level]
|
|
13
|
+
log_level = options[:log_level]
|
|
14
14
|
|
|
15
15
|
# Load settings early, before any legion-* gem requires can trigger auto-load.
|
|
16
16
|
# This ensures DNS bootstrap and config file loading happen exactly once.
|
|
@@ -26,7 +26,8 @@ module Legion
|
|
|
26
26
|
clear_log_file unless options[:daemonize]
|
|
27
27
|
|
|
28
28
|
api = options.fetch(:api, true)
|
|
29
|
-
service_opts = {
|
|
29
|
+
service_opts = { api: api }
|
|
30
|
+
service_opts[:log_level] = log_level if log_level
|
|
30
31
|
service_opts[:http_port] = options[:http_port] if options[:http_port]
|
|
31
32
|
service_opts[:role] = :lite if options[:lite]
|
|
32
33
|
Legion.instance_variable_set(:@service, Legion::Service.new(**service_opts))
|
data/lib/legion/cli.rb
CHANGED
|
@@ -90,7 +90,7 @@ module Legion
|
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
def self.start(given_args = ARGV, config = {})
|
|
93
|
-
super
|
|
93
|
+
super(normalize_help_args(given_args), config)
|
|
94
94
|
rescue Legion::CLI::Error => e
|
|
95
95
|
Legion::Logging.error("CLI::Main.start CLI error: #{e.message}") if defined?(Legion::Logging)
|
|
96
96
|
formatter = Output::Formatter.new(json: given_args.include?('--json'), color: !given_args.include?('--no-color'))
|
|
@@ -106,6 +106,17 @@ module Legion
|
|
|
106
106
|
exit(1)
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
+
def self.normalize_help_args(given_args)
|
|
110
|
+
args = Array(given_args).dup
|
|
111
|
+
return args unless args.length == 2
|
|
112
|
+
return args unless %w[--help -h].include?(args.last)
|
|
113
|
+
|
|
114
|
+
command = args.first
|
|
115
|
+
return args if command.start_with?('-') || command == 'help'
|
|
116
|
+
|
|
117
|
+
['help', command]
|
|
118
|
+
end
|
|
119
|
+
|
|
109
120
|
LEGION_GEMS = %w[
|
|
110
121
|
legion-transport legion-cache legion-crypt legion-data
|
|
111
122
|
legion-json legion-logging legion-settings
|
|
@@ -163,7 +174,7 @@ module Legion
|
|
|
163
174
|
option :pidfile, type: :string, aliases: ['-p'], desc: 'PID file path'
|
|
164
175
|
option :logfile, type: :string, aliases: ['-l'], desc: 'Log file path'
|
|
165
176
|
option :time_limit, type: :numeric, aliases: ['-t'], desc: 'Run for N seconds then exit'
|
|
166
|
-
option :log_level, type: :string,
|
|
177
|
+
option :log_level, type: :string, desc: 'Log level (debug, info, warn, error)'
|
|
167
178
|
option :api, type: :boolean, default: true, desc: 'Start the HTTP API server'
|
|
168
179
|
option :http_port, type: :numeric, desc: 'HTTP API port (overrides settings)'
|
|
169
180
|
option :lite, type: :boolean, default: false, desc: 'Start in lite mode (no external services)'
|
|
@@ -56,6 +56,9 @@ module Legion
|
|
|
56
56
|
|
|
57
57
|
def reset!
|
|
58
58
|
@entries = {}
|
|
59
|
+
@extension_catalog_available = nil
|
|
60
|
+
@extension_catalog_connection_id = nil
|
|
61
|
+
@warned_missing_extension_catalog = false
|
|
59
62
|
end
|
|
60
63
|
|
|
61
64
|
private
|
|
@@ -69,13 +72,17 @@ module Legion
|
|
|
69
72
|
Legion::Transport::Connection.respond_to?(:session_open?) &&
|
|
70
73
|
Legion::Transport::Connection.session_open?
|
|
71
74
|
|
|
72
|
-
Legion::
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
)
|
|
75
|
+
payload = Legion::JSON.dump(
|
|
76
|
+
lex_name: lex_name,
|
|
77
|
+
state: new_state.to_s,
|
|
78
|
+
timestamp: Time.now.to_i
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
exchange = Legion::Transport::Exchange.new('legion.catalog')
|
|
82
|
+
exchange.publish(payload, routing_key: "legion.catalog.#{lex_name}.#{new_state}",
|
|
83
|
+
content_type: 'application/json', persistent: true)
|
|
77
84
|
rescue StandardError => e
|
|
78
|
-
Legion::Logging.
|
|
85
|
+
Legion::Logging.warn { "Catalog publish failed for #{lex_name}=#{new_state}: #{e.class}: #{e.message}" } if defined?(Legion::Logging)
|
|
79
86
|
end
|
|
80
87
|
|
|
81
88
|
def persist_transition(lex_name, new_state)
|
|
@@ -83,6 +90,9 @@ module Legion
|
|
|
83
90
|
Legion::Data::Local.respond_to?(:connected?) &&
|
|
84
91
|
Legion::Data::Local.connected?
|
|
85
92
|
|
|
93
|
+
ensure_local_migration_registered!
|
|
94
|
+
return warn_missing_extension_catalog_once unless extension_catalog_table_available?
|
|
95
|
+
|
|
86
96
|
model = Legion::Data::Local.model(:extension_catalog)
|
|
87
97
|
existing = model.where(lex_name: lex_name).first
|
|
88
98
|
if existing
|
|
@@ -91,14 +101,70 @@ module Legion
|
|
|
91
101
|
model.insert(lex_name: lex_name, state: new_state.to_s, created_at: Time.now, updated_at: Time.now)
|
|
92
102
|
end
|
|
93
103
|
rescue StandardError => e
|
|
94
|
-
Legion::Logging.
|
|
104
|
+
Legion::Logging.warn { "Catalog persist failed for #{lex_name}=#{new_state}: #{e.class}: #{e.message}" } if defined?(Legion::Logging)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def extension_catalog_table_available?
|
|
108
|
+
connection = Legion::Data::Local.connection
|
|
109
|
+
return false unless connection
|
|
110
|
+
|
|
111
|
+
connection_id = connection.object_id
|
|
112
|
+
return true if @extension_catalog_connection_id == connection_id && @extension_catalog_available == true
|
|
113
|
+
|
|
114
|
+
available =
|
|
115
|
+
if connection.respond_to?(:tables)
|
|
116
|
+
connection.tables.include?(:extension_catalog)
|
|
117
|
+
else
|
|
118
|
+
connection.respond_to?(:table_exists?) && connection.table_exists?(:extension_catalog)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
if available
|
|
122
|
+
@extension_catalog_connection_id = connection_id
|
|
123
|
+
@extension_catalog_available = true
|
|
124
|
+
else
|
|
125
|
+
@extension_catalog_connection_id = nil if @extension_catalog_connection_id == connection_id
|
|
126
|
+
@extension_catalog_available = nil
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
available
|
|
130
|
+
rescue StandardError => e
|
|
131
|
+
Legion::Logging.warn { "Catalog table availability check failed: #{e.class}: #{e.message}" } if defined?(Legion::Logging)
|
|
132
|
+
false
|
|
95
133
|
end
|
|
96
|
-
end
|
|
97
134
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
135
|
+
def ensure_local_migration_registered!
|
|
136
|
+
return unless defined?(Legion::Data::Local) &&
|
|
137
|
+
Legion::Data::Local.respond_to?(:register_migrations)
|
|
138
|
+
|
|
139
|
+
path = extension_catalog_migrations_path
|
|
140
|
+
return unless Dir.exist?(path)
|
|
141
|
+
|
|
142
|
+
registered = if Legion::Data::Local.respond_to?(:registered_migrations)
|
|
143
|
+
Legion::Data::Local.registered_migrations
|
|
144
|
+
else
|
|
145
|
+
{}
|
|
146
|
+
end
|
|
147
|
+
return if registered.is_a?(Hash) && registered.key?(:extension_catalog)
|
|
148
|
+
|
|
149
|
+
Legion::Data::Local.register_migrations(name: :extension_catalog, path: path)
|
|
150
|
+
rescue StandardError => e
|
|
151
|
+
Legion::Logging.warn { "Catalog migration registration failed: #{e.class}: #{e.message}" } if defined?(Legion::Logging)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def extension_catalog_migrations_path
|
|
155
|
+
File.expand_path('../data/local_migrations', __dir__)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def warn_missing_extension_catalog_once
|
|
159
|
+
return false if @warned_missing_extension_catalog
|
|
160
|
+
|
|
161
|
+
@warned_missing_extension_catalog = true
|
|
162
|
+
Legion::Logging.warn('Catalog persist skipped: extension_catalog table is missing in Legion::Data::Local') if defined?(Legion::Logging)
|
|
163
|
+
false
|
|
164
|
+
end
|
|
101
165
|
end
|
|
166
|
+
|
|
167
|
+
send(:ensure_local_migration_registered!) if defined?(Legion::Data::Local)
|
|
102
168
|
end
|
|
103
169
|
end
|
|
104
170
|
end
|
|
@@ -189,23 +189,25 @@ module Legion
|
|
|
189
189
|
end
|
|
190
190
|
|
|
191
191
|
def build_settings
|
|
192
|
+
defaults = deep_dup_settings_value(Legion::Settings[:default_extension_settings] || {})
|
|
193
|
+
|
|
192
194
|
if Legion::Settings[:extensions].key?(lex_name.to_sym)
|
|
193
|
-
|
|
195
|
+
defaults.each do |key, value|
|
|
194
196
|
Legion::Settings[:extensions][lex_name.to_sym][key.to_sym] = if Legion::Settings[:extensions][lex_name.to_sym].key?(key.to_sym)
|
|
195
|
-
value.merge(Legion::Settings[:extensions][lex_name.to_sym][key.to_sym])
|
|
197
|
+
deep_dup_settings_value(value).merge(Legion::Settings[:extensions][lex_name.to_sym][key.to_sym])
|
|
196
198
|
else
|
|
197
|
-
value
|
|
199
|
+
deep_dup_settings_value(value)
|
|
198
200
|
end
|
|
199
201
|
end
|
|
200
202
|
else
|
|
201
|
-
Legion::Settings[:extensions][lex_name.to_sym] =
|
|
203
|
+
Legion::Settings[:extensions][lex_name.to_sym] = defaults
|
|
202
204
|
end
|
|
203
205
|
|
|
204
206
|
default_settings.each do |key, value|
|
|
205
207
|
Legion::Settings[:extensions][lex_name.to_sym][key.to_sym] = if Legion::Settings[:extensions][lex_name.to_sym].key?(key.to_sym)
|
|
206
|
-
value.merge(Legion::Settings[:extensions][lex_name.to_sym][key.to_sym])
|
|
208
|
+
deep_dup_settings_value(value).merge(Legion::Settings[:extensions][lex_name.to_sym][key.to_sym])
|
|
207
209
|
else
|
|
208
|
-
value
|
|
210
|
+
deep_dup_settings_value(value)
|
|
209
211
|
end
|
|
210
212
|
end
|
|
211
213
|
end
|
|
@@ -237,6 +239,21 @@ module Legion
|
|
|
237
239
|
rescue StandardError => e
|
|
238
240
|
handle_exception(e, lex: lex_name, operation: 'auto_generate_data')
|
|
239
241
|
end
|
|
242
|
+
|
|
243
|
+
def deep_dup_settings_value(value)
|
|
244
|
+
case value
|
|
245
|
+
when Hash
|
|
246
|
+
value.each_with_object({}) do |(key, nested), duplicated|
|
|
247
|
+
duplicated[key.to_sym] = deep_dup_settings_value(nested)
|
|
248
|
+
end
|
|
249
|
+
when Array
|
|
250
|
+
value.map { |item| deep_dup_settings_value(item) }
|
|
251
|
+
else
|
|
252
|
+
value.dup
|
|
253
|
+
end
|
|
254
|
+
rescue TypeError
|
|
255
|
+
value
|
|
256
|
+
end
|
|
240
257
|
end
|
|
241
258
|
end
|
|
242
259
|
end
|
|
@@ -78,6 +78,8 @@ module Legion
|
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
def vault_connected?
|
|
81
|
+
return Legion::Crypt.vault_connected? if defined?(Legion::Crypt) && Legion::Crypt.respond_to?(:vault_connected?)
|
|
82
|
+
|
|
81
83
|
defined?(Legion::Settings) &&
|
|
82
84
|
Legion::Settings[:crypt]&.dig(:vault, :connected) == true
|
|
83
85
|
rescue StandardError
|