legionio 1.4.53 → 1.4.58
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 +46 -0
- data/lib/legion/cli/config_command.rb +3 -2
- data/lib/legion/cli/lex_command.rb +193 -115
- data/lib/legion/extensions/core.rb +1 -0
- data/lib/legion/extensions/helpers/base.rb +74 -4
- data/lib/legion/extensions/helpers/logger.rb +5 -2
- data/lib/legion/extensions/helpers/segments.rb +62 -0
- data/lib/legion/extensions/helpers/transport.rb +6 -5
- data/lib/legion/extensions/transport.rb +2 -3
- data/lib/legion/extensions.rb +202 -86
- data/lib/legion/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b941e35cffdc0cee05b505548896ec05866dc24c517f42ecc4d9a3f16a77c53f
|
|
4
|
+
data.tar.gz: f0e5f2a91dc3627a3124bd9cbe74fb02ed235fad0fee966b221e48acc935bee7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 67561fe5e20de1e404b63218fa971989d44767ff9df893ddc922598e94086e14b5134884c434264bb47449065018c8fe99dad88274c3ed629be0f98345ac0036
|
|
7
|
+
data.tar.gz: fcb1fbe1f834f856410ce41d319dabc115bbbccbd23562783d26cc0efc8672c09eae205a5e7c376533744008cd3675f8b2208b393ba2e0dbc6335d4cb8e27ed8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# Legion Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.58] - 2026-03-17
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `legion lex list` now groups output by category (tier order) by default.
|
|
7
|
+
- `legion lex list CATEGORY` filters the list to a specific category (e.g., `legion lex list agentic`).
|
|
8
|
+
- `--flat` option to `legion lex list` restores the original flat table without grouping.
|
|
9
|
+
- `category` and `tier` columns added to the extension table in all display modes.
|
|
10
|
+
- `discover_all` now includes `:category` and `:tier` keys in each extension info hash,
|
|
11
|
+
derived via `Legion::Extensions::Helpers::Segments.categorize_gem`.
|
|
12
|
+
- Results sorted by tier then name for deterministic ordering.
|
|
13
|
+
|
|
14
|
+
## [1.4.57] - 2026-03-17
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- `--category` option to `legion lex create`: generates categorized extension gems with nested module
|
|
18
|
+
declarations, nested directory structure, and correct `VERSION` constant paths.
|
|
19
|
+
Example: `legion lex create cognitive-anchor --category agentic` produces gem `lex-agentic-cognitive-anchor`
|
|
20
|
+
with module `Legion::Extensions::Agentic::Cognitive::Anchor`.
|
|
21
|
+
- `LexGenerator` now accepts `gem_name:` keyword argument and uses `Legion::Extensions::Helpers::Segments`
|
|
22
|
+
to derive all namespace, const, and require-path values for both flat and nested extensions.
|
|
23
|
+
- `legion lex create` emits a warning via `Legion::Extensions.check_reserved_words` when reserved
|
|
24
|
+
category prefixes or framework words are used in the gem name.
|
|
25
|
+
|
|
26
|
+
## [1.4.56] - 2026-03-17
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- `lex_class` now returns the full extension module constant by walking the namespace up to the first `NAMESPACE_BOUNDARIES` word, instead of always stopping at index 2. For nested extensions (`Legion::Extensions::Agentic::Cognitive::Anchor`), this returns `Legion::Extensions::Agentic::Cognitive::Anchor` rather than the incorrect `Legion::Extensions::Agentic`.
|
|
30
|
+
- `lex_const` now derives from `lex_class.to_s.split('::').last` so it returns the extension's root constant name (`Anchor`) rather than always returning the third element of the namespace array.
|
|
31
|
+
- `full_path` now builds the gem name from dash-joined segments (`lex-agentic-cognitive-anchor`) instead of underscore-joined `lex_name`, so `Gem::Specification.find_by_name` works for nested extensions.
|
|
32
|
+
|
|
33
|
+
## [1.4.55] - 2026-03-17
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
- `build_default_exchange` now sets `exchange_name` on dynamically created exchange classes to return `amqp_prefix` (dot-joined segments with `legion.` prefix) instead of defaulting to the parent class behavior
|
|
37
|
+
- `auto_create_exchange` now derives `exchange_name` from `amqp_prefix` + the exchange's own downcased class name, replacing the index-based `split('::')[5].downcase` extraction that broke for nested extension namespaces
|
|
38
|
+
### Fixed
|
|
39
|
+
- `legion config scaffold` now writes to `~/.legionio/settings/` by default instead of `./settings/`
|
|
40
|
+
- Removed Thor `default: './settings'` that shadowed the Ruby fallback in `ConfigScaffold.run`
|
|
41
|
+
- Added `~/.legionio/settings` to `legion config path` search paths to match `Service#default_paths`
|
|
42
|
+
|
|
43
|
+
## [1.4.54] - 2026-03-17
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
- `Helpers::Logger#log` now passes `lex_segments:` array to `Legion::Logging::Logger` when the object responds to `:segments`
|
|
47
|
+
- Falls back to `lex:` string for legacy flat extensions that do not implement `:segments`
|
|
48
|
+
|
|
3
49
|
## [1.4.53] - 2026-03-17
|
|
4
50
|
|
|
5
51
|
### Fixed
|
|
@@ -167,12 +167,12 @@ module Legion
|
|
|
167
167
|
desc 'scaffold', 'Generate starter config files for each subsystem'
|
|
168
168
|
long_desc <<~DESC
|
|
169
169
|
Generates JSON config files for LegionIO subsystems (transport, data, cache,
|
|
170
|
-
crypt, logging, llm). Files are written to --dir (default:
|
|
170
|
+
crypt, logging, llm). Files are written to --dir (default: ~/.legionio/settings/).
|
|
171
171
|
|
|
172
172
|
By default, generates minimal starter files with only the most commonly
|
|
173
173
|
changed fields. Use --full for the complete schema with all defaults.
|
|
174
174
|
DESC
|
|
175
|
-
option :dir, type: :string,
|
|
175
|
+
option :dir, type: :string, desc: 'Output directory (default: ~/.legionio/settings)'
|
|
176
176
|
option :only, type: :string, desc: 'Comma-separated subsystems (transport,data,cache,crypt,logging,llm)'
|
|
177
177
|
option :full, type: :boolean, default: false, desc: 'Include all fields with defaults'
|
|
178
178
|
option :force, type: :boolean, default: false, desc: 'Overwrite existing files'
|
|
@@ -194,6 +194,7 @@ module Legion
|
|
|
194
194
|
active_found = false
|
|
195
195
|
[
|
|
196
196
|
'/etc/legionio',
|
|
197
|
+
File.expand_path('~/.legionio/settings'),
|
|
197
198
|
File.expand_path('~/legionio'),
|
|
198
199
|
File.expand_path('./settings')
|
|
199
200
|
].map do |path|
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'fileutils'
|
|
4
|
+
require 'legion/extensions/helpers/segments'
|
|
4
5
|
|
|
5
6
|
module Legion
|
|
6
7
|
module CLI
|
|
7
8
|
class Lex < Thor
|
|
9
|
+
DEFAULT_CATEGORIES = {
|
|
10
|
+
core: { type: :list, tier: 1 },
|
|
11
|
+
ai: { type: :list, tier: 2 },
|
|
12
|
+
gaia: { type: :list, tier: 3 },
|
|
13
|
+
agentic: { type: :prefix, tier: 4 }
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
8
16
|
def self.exit_on_failure?
|
|
9
17
|
true
|
|
10
18
|
end
|
|
@@ -12,32 +20,21 @@ module Legion
|
|
|
12
20
|
class_option :json, type: :boolean, default: false, desc: 'Output as JSON'
|
|
13
21
|
class_option :no_color, type: :boolean, default: false, desc: 'Disable color output'
|
|
14
22
|
|
|
15
|
-
desc 'list', 'List all installed extensions'
|
|
16
|
-
option :all,
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
desc 'list [CATEGORY]', 'List all installed extensions, optionally filtered by category'
|
|
24
|
+
option :all, type: :boolean, default: false, aliases: ['-a'], desc: 'Include disabled extensions'
|
|
25
|
+
option :flat, type: :boolean, default: false, desc: 'Show all extensions in a flat list without category grouping'
|
|
26
|
+
def list(category = nil)
|
|
27
|
+
out = formatter
|
|
19
28
|
lexs = discover_all
|
|
20
29
|
|
|
21
|
-
rows =
|
|
22
|
-
|
|
23
|
-
else
|
|
24
|
-
lexs.reject { |l| l[:status] == 'disabled' }
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
table_rows = rows.map do |l|
|
|
28
|
-
[
|
|
29
|
-
l[:name],
|
|
30
|
-
l[:version],
|
|
31
|
-
out.status(l[:status]),
|
|
32
|
-
l[:runners].to_s,
|
|
33
|
-
l[:actors].to_s
|
|
34
|
-
]
|
|
35
|
-
end
|
|
30
|
+
rows = options[:all] ? lexs : lexs.reject { |l| l[:status] == 'disabled' }
|
|
31
|
+
rows = rows.select { |l| l[:category] == category } if category
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
if options[:flat] || category
|
|
34
|
+
render_flat_table(out, rows)
|
|
35
|
+
else
|
|
36
|
+
render_grouped_table(out, rows)
|
|
37
|
+
end
|
|
41
38
|
end
|
|
42
39
|
default_task :list
|
|
43
40
|
|
|
@@ -92,13 +89,22 @@ module Legion
|
|
|
92
89
|
end
|
|
93
90
|
|
|
94
91
|
desc 'create NAME', 'Scaffold a new Legion extension'
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
method_option :rspec, type: :boolean, default: true, desc: 'Include RSpec setup'
|
|
93
|
+
method_option :github_ci, type: :boolean, default: true, desc: 'Include GitHub Actions CI'
|
|
94
|
+
method_option :git_init, type: :boolean, default: true, desc: 'Initialize git repository'
|
|
95
|
+
method_option :bundle_install, type: :boolean, default: true, desc: 'Run bundle install'
|
|
96
|
+
method_option :category, type: :string, default: nil,
|
|
97
|
+
desc: 'Extension category (agentic, ai, gaia). Determines namespace nesting and gem prefix.'
|
|
99
98
|
def create(name)
|
|
100
99
|
out = formatter
|
|
101
|
-
|
|
100
|
+
|
|
101
|
+
if options[:category] && options[:category] !~ /\A[a-z][a-z0-9_-]*\z/
|
|
102
|
+
out.error('--category must be lowercase letters, numbers, underscores, or hyphens')
|
|
103
|
+
return
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
gem_name = options[:category] ? "lex-#{options[:category]}-#{name}" : "lex-#{name}"
|
|
107
|
+
target_dir = gem_name
|
|
102
108
|
|
|
103
109
|
if Dir.exist?(target_dir)
|
|
104
110
|
out.error("Directory #{target_dir} already exists")
|
|
@@ -110,15 +116,17 @@ module Legion
|
|
|
110
116
|
raise SystemExit, 1
|
|
111
117
|
end
|
|
112
118
|
|
|
113
|
-
|
|
119
|
+
Legion::Extensions.check_reserved_words(gem_name, known_org: false)
|
|
120
|
+
|
|
121
|
+
out.success("Creating #{gem_name}...")
|
|
114
122
|
|
|
115
123
|
vars = { filename: target_dir, class_name: name.split('_').map(&:capitalize).join, lex: name }
|
|
116
124
|
|
|
117
|
-
generator = LexGenerator.new(name, vars, options)
|
|
125
|
+
generator = LexGenerator.new(name, vars, options, gem_name: gem_name)
|
|
118
126
|
generator.generate(out)
|
|
119
127
|
|
|
120
128
|
out.spacer
|
|
121
|
-
out.success("Extension
|
|
129
|
+
out.success("Extension #{gem_name} created in ./#{target_dir}")
|
|
122
130
|
out.spacer
|
|
123
131
|
puts ' Next steps:'
|
|
124
132
|
puts " cd #{target_dir}"
|
|
@@ -166,6 +174,25 @@ module Legion
|
|
|
166
174
|
)
|
|
167
175
|
end
|
|
168
176
|
|
|
177
|
+
def render_flat_table(out, rows)
|
|
178
|
+
table_rows = rows.map do |l|
|
|
179
|
+
[l[:name], l[:version], l[:category].to_s, l[:tier].to_s, out.status(l[:status]), l[:runners].to_s, l[:actors].to_s]
|
|
180
|
+
end
|
|
181
|
+
out.table(%w[name version category tier status runners actors], table_rows)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def render_grouped_table(out, rows)
|
|
185
|
+
grouped = rows.group_by { |l| [l[:tier], l[:category]] }
|
|
186
|
+
grouped.keys.sort_by { |tier, cat| [tier, cat.to_s] }.each do |key|
|
|
187
|
+
tier, cat = key
|
|
188
|
+
out.header("=== #{cat} (tier #{tier}) ===")
|
|
189
|
+
group_rows = grouped[key].map do |l|
|
|
190
|
+
[l[:name], l[:version], l[:category].to_s, l[:tier].to_s, out.status(l[:status]), l[:runners].to_s, l[:actors].to_s]
|
|
191
|
+
end
|
|
192
|
+
out.table(%w[name version category tier status runners actors], group_rows)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
169
196
|
def discover_all
|
|
170
197
|
installed = Gem::Specification.select { |s| s.name.start_with?('lex-') }
|
|
171
198
|
|
|
@@ -177,20 +204,19 @@ module Legion
|
|
|
177
204
|
ext_settings = {}
|
|
178
205
|
end
|
|
179
206
|
|
|
207
|
+
categories = resolve_categories
|
|
208
|
+
cat_lists = resolve_cat_lists
|
|
209
|
+
|
|
180
210
|
result = installed.map do |spec|
|
|
181
211
|
short_name = spec.name.sub('lex-', '')
|
|
182
|
-
|
|
183
|
-
extension_class = "Legion::Extensions::#{class_name}"
|
|
212
|
+
extension_class = Legion::Extensions::Helpers::Segments.derive_const_path(spec.name)
|
|
184
213
|
|
|
185
214
|
setting = ext_settings[short_name.to_sym] || {}
|
|
186
|
-
status
|
|
187
|
-
'disabled'
|
|
188
|
-
else
|
|
189
|
-
'installed'
|
|
190
|
-
end
|
|
215
|
+
status = setting[:enabled] == false ? 'disabled' : 'installed'
|
|
191
216
|
|
|
192
217
|
runner_info = extract_runners(spec)
|
|
193
|
-
actor_info
|
|
218
|
+
actor_info = extract_actors(spec)
|
|
219
|
+
cat_info = Legion::Extensions::Helpers::Segments.categorize_gem(spec.name, categories: categories, lists: cat_lists)
|
|
194
220
|
|
|
195
221
|
{
|
|
196
222
|
name: short_name,
|
|
@@ -200,10 +226,25 @@ module Legion
|
|
|
200
226
|
extension_class: extension_class,
|
|
201
227
|
runners: runner_info,
|
|
202
228
|
actors: actor_info,
|
|
203
|
-
dependencies: spec.runtime_dependencies.map(&:to_s)
|
|
229
|
+
dependencies: spec.runtime_dependencies.map(&:to_s),
|
|
230
|
+
category: cat_info[:category].to_s,
|
|
231
|
+
tier: cat_info[:tier]
|
|
204
232
|
}
|
|
205
233
|
end
|
|
206
|
-
result.sort_by { |l| l[:name] }
|
|
234
|
+
result.sort_by { |l| [l[:tier], l[:name]] }
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def resolve_categories
|
|
238
|
+
raw = Legion::Settings.dig(:extensions, :categories)
|
|
239
|
+
raw.nil? || raw.empty? ? DEFAULT_CATEGORIES : raw
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def resolve_cat_lists
|
|
243
|
+
{
|
|
244
|
+
core: Array(Legion::Settings.dig(:extensions, :core)),
|
|
245
|
+
ai: Array(Legion::Settings.dig(:extensions, :ai)),
|
|
246
|
+
gaia: Array(Legion::Settings.dig(:extensions, :gaia))
|
|
247
|
+
}
|
|
207
248
|
end
|
|
208
249
|
|
|
209
250
|
def find_lex(name)
|
|
@@ -212,7 +253,8 @@ module Legion
|
|
|
212
253
|
end
|
|
213
254
|
|
|
214
255
|
def extract_runners(spec)
|
|
215
|
-
runner_dir = File.join(spec.gem_dir, 'lib', 'legion', 'extensions',
|
|
256
|
+
runner_dir = File.join(spec.gem_dir, 'lib', 'legion', 'extensions',
|
|
257
|
+
Legion::Extensions::Helpers::Segments.derive_segments(spec.name).join('/'), 'runners')
|
|
216
258
|
return [] unless Dir.exist?(runner_dir)
|
|
217
259
|
|
|
218
260
|
Dir.glob("#{runner_dir}/*.rb").map { |f| File.basename(f, '.rb') }
|
|
@@ -221,7 +263,8 @@ module Legion
|
|
|
221
263
|
end
|
|
222
264
|
|
|
223
265
|
def extract_actors(spec)
|
|
224
|
-
actor_dir = File.join(spec.gem_dir, 'lib', 'legion', 'extensions',
|
|
266
|
+
actor_dir = File.join(spec.gem_dir, 'lib', 'legion', 'extensions',
|
|
267
|
+
Legion::Extensions::Helpers::Segments.derive_segments(spec.name).join('/'), 'actors')
|
|
225
268
|
return [] unless Dir.exist?(actor_dir)
|
|
226
269
|
|
|
227
270
|
Dir.glob("#{actor_dir}/*.rb").map do |f|
|
|
@@ -255,11 +298,12 @@ module Legion
|
|
|
255
298
|
|
|
256
299
|
# Thin generator class that wraps the template logic
|
|
257
300
|
class LexGenerator
|
|
258
|
-
def initialize(name, vars, options)
|
|
259
|
-
@name
|
|
260
|
-
@vars
|
|
301
|
+
def initialize(name, vars, options, gem_name: nil)
|
|
302
|
+
@name = name
|
|
303
|
+
@vars = vars
|
|
261
304
|
@options = options
|
|
262
|
-
@
|
|
305
|
+
@gem_name = gem_name || "lex-#{name}"
|
|
306
|
+
@target = @gem_name
|
|
263
307
|
end
|
|
264
308
|
|
|
265
309
|
def generate(out)
|
|
@@ -270,24 +314,76 @@ module Legion
|
|
|
270
314
|
|
|
271
315
|
private
|
|
272
316
|
|
|
273
|
-
|
|
317
|
+
attr_reader :gem_name
|
|
318
|
+
|
|
319
|
+
def target_dir
|
|
320
|
+
@target
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def namespace_segments
|
|
324
|
+
@namespace_segments ||= Legion::Extensions::Helpers::Segments.derive_namespace(@gem_name)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def const_path
|
|
328
|
+
@const_path ||= Legion::Extensions::Helpers::Segments.derive_const_path(@gem_name)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def require_path
|
|
332
|
+
@require_path ||= Legion::Extensions::Helpers::Segments.derive_require_path(@gem_name)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def extension_dirs
|
|
336
|
+
base = "#{@target}/lib/legion/extensions"
|
|
337
|
+
segs = Legion::Extensions::Helpers::Segments.derive_segments(@gem_name)
|
|
274
338
|
dirs = [
|
|
275
339
|
@target,
|
|
276
340
|
"#{@target}/lib",
|
|
277
341
|
"#{@target}/lib/legion",
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
"#{
|
|
282
|
-
|
|
342
|
+
base
|
|
343
|
+
]
|
|
344
|
+
segs.each_with_index do |_, i|
|
|
345
|
+
dirs << "#{base}/#{segs[0..i].join('/')}"
|
|
346
|
+
end
|
|
347
|
+
dirs += [
|
|
348
|
+
"#{base}/#{segs.join('/')}/runners",
|
|
349
|
+
"#{base}/#{segs.join('/')}/actors",
|
|
350
|
+
"#{base}/#{segs.join('/')}/tools",
|
|
283
351
|
"#{@target}/spec",
|
|
284
352
|
"#{@target}/spec/legion"
|
|
285
353
|
]
|
|
354
|
+
dirs
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def module_open_lines
|
|
358
|
+
indent = ' '
|
|
359
|
+
lines = ["module Legion\n", "#{indent}module Extensions\n"]
|
|
360
|
+
namespace_segments.each_with_index do |seg, i|
|
|
361
|
+
lines << "#{indent * (i + 2)}module #{seg}\n"
|
|
362
|
+
end
|
|
363
|
+
lines
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def module_close_lines
|
|
367
|
+
depth = namespace_segments.length + 2
|
|
368
|
+
(1..depth).map { |i| "#{' ' * (depth - i)}end\n" }
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def nested_module_wrap(inner_lines)
|
|
372
|
+
opens = module_open_lines
|
|
373
|
+
closes = module_close_lines
|
|
374
|
+
(opens + inner_lines + closes).join
|
|
375
|
+
end
|
|
286
376
|
|
|
377
|
+
def create_structure(out)
|
|
378
|
+
dirs = extension_dirs
|
|
287
379
|
dirs << "#{@target}/.github/workflows" if @options[:github_ci]
|
|
288
380
|
|
|
289
381
|
dirs.each { |d| FileUtils.mkdir_p(d) }
|
|
290
|
-
|
|
382
|
+
|
|
383
|
+
ext_base = "lib/legion/extensions/#{Legion::Extensions::Helpers::Segments.derive_segments(@gem_name).join('/')}"
|
|
384
|
+
FileUtils.touch("#{@target}/#{ext_base}/tools/.gitkeep")
|
|
385
|
+
|
|
386
|
+
entry_file = "lib/legion/extensions/#{require_path.split('legion/extensions/').last}"
|
|
291
387
|
|
|
292
388
|
write_template("#{@target}/#{@target}.gemspec", gemspec_content)
|
|
293
389
|
write_template("#{@target}/Gemfile", gemfile_content)
|
|
@@ -295,13 +391,15 @@ module Legion
|
|
|
295
391
|
write_template("#{@target}/.rubocop.yml", rubocop_content)
|
|
296
392
|
write_template("#{@target}/LICENSE", license_content)
|
|
297
393
|
write_template("#{@target}/README.md", readme_content)
|
|
298
|
-
write_template("#{@target}/lib
|
|
299
|
-
write_template("#{@target}
|
|
300
|
-
write_template("#{@target}
|
|
394
|
+
write_template("#{@target}/lib/#{entry_file}.rb", extension_entry_content)
|
|
395
|
+
write_template("#{@target}/#{ext_base}/version.rb", version_content)
|
|
396
|
+
write_template("#{@target}/#{ext_base}/client.rb", client_content)
|
|
301
397
|
|
|
302
398
|
if @options[:rspec]
|
|
399
|
+
spec_relative = Legion::Extensions::Helpers::Segments.derive_segments(@gem_name).join('/')
|
|
400
|
+
FileUtils.mkdir_p("#{@target}/spec/legion/extensions/#{File.dirname(spec_relative)}")
|
|
303
401
|
write_template("#{@target}/spec/spec_helper.rb", spec_helper_content)
|
|
304
|
-
write_template("#{@target}/spec/legion/#{
|
|
402
|
+
write_template("#{@target}/spec/legion/extensions/#{spec_relative}_spec.rb", spec_content)
|
|
305
403
|
end
|
|
306
404
|
|
|
307
405
|
if @options[:github_ci]
|
|
@@ -336,16 +434,16 @@ module Legion
|
|
|
336
434
|
<<~RUBY
|
|
337
435
|
# frozen_string_literal: true
|
|
338
436
|
|
|
339
|
-
require_relative 'lib
|
|
437
|
+
require_relative 'lib/#{require_path}/version'
|
|
340
438
|
|
|
341
439
|
Gem::Specification.new do |spec|
|
|
342
|
-
spec.name = '#{@
|
|
343
|
-
spec.version =
|
|
440
|
+
spec.name = '#{@gem_name}'
|
|
441
|
+
spec.version = #{const_path}::VERSION
|
|
344
442
|
spec.authors = ['Esity']
|
|
345
443
|
spec.email = ['matthewdiverson@gmail.com']
|
|
346
|
-
spec.summary = 'A LegionIO Extension for #{
|
|
347
|
-
spec.description = 'A LegionIO Extension (LEX) for #{
|
|
348
|
-
spec.homepage = 'https://github.com/LegionIO/#{@
|
|
444
|
+
spec.summary = 'A LegionIO Extension for #{namespace_segments.last}'
|
|
445
|
+
spec.description = 'A LegionIO Extension (LEX) for #{namespace_segments.last}'
|
|
446
|
+
spec.homepage = 'https://github.com/LegionIO/#{@gem_name}'
|
|
349
447
|
spec.license = 'MIT'
|
|
350
448
|
spec.required_ruby_version = '>= 3.4'
|
|
351
449
|
|
|
@@ -432,14 +530,14 @@ module Legion
|
|
|
432
530
|
|
|
433
531
|
def readme_content
|
|
434
532
|
<<~MD
|
|
435
|
-
#
|
|
533
|
+
# #{@gem_name}
|
|
436
534
|
|
|
437
|
-
A [LegionIO](https://github.com/LegionIO) extension for #{
|
|
535
|
+
A [LegionIO](https://github.com/LegionIO) extension for #{namespace_segments.last}.
|
|
438
536
|
|
|
439
537
|
## Installation
|
|
440
538
|
|
|
441
539
|
```ruby
|
|
442
|
-
gem '
|
|
540
|
+
gem '#{@gem_name}'
|
|
443
541
|
```
|
|
444
542
|
|
|
445
543
|
## Usage
|
|
@@ -461,64 +559,44 @@ module Legion
|
|
|
461
559
|
end
|
|
462
560
|
|
|
463
561
|
def extension_entry_content
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
module Legion
|
|
471
|
-
module Extensions
|
|
472
|
-
module #{@vars[:class_name]}
|
|
473
|
-
end
|
|
474
|
-
end
|
|
475
|
-
end
|
|
476
|
-
RUBY
|
|
562
|
+
segs = Legion::Extensions::Helpers::Segments.derive_segments(@gem_name)
|
|
563
|
+
last_seg = segs.last
|
|
564
|
+
inner = [" require_relative '#{last_seg}/version'\n",
|
|
565
|
+
" require_relative '#{last_seg}/client'\n",
|
|
566
|
+
"\n"]
|
|
567
|
+
"# frozen_string_literal: true\n\n#{nested_module_wrap(inner)}"
|
|
477
568
|
end
|
|
478
569
|
|
|
479
570
|
def version_content
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
module Legion
|
|
484
|
-
module Extensions
|
|
485
|
-
module #{@vars[:class_name]}
|
|
486
|
-
VERSION = '0.1.0'
|
|
487
|
-
end
|
|
488
|
-
end
|
|
489
|
-
end
|
|
490
|
-
RUBY
|
|
571
|
+
depth = namespace_segments.length + 2
|
|
572
|
+
inner = ["#{' ' * depth}VERSION = '0.1.0'\n"]
|
|
573
|
+
"# frozen_string_literal: true\n\n#{nested_module_wrap(inner)}"
|
|
491
574
|
end
|
|
492
575
|
|
|
493
576
|
def client_content
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
end
|
|
511
|
-
end
|
|
512
|
-
end
|
|
513
|
-
end
|
|
514
|
-
RUBY
|
|
577
|
+
depth = namespace_segments.length + 2
|
|
578
|
+
pad = ' ' * depth
|
|
579
|
+
inner = [
|
|
580
|
+
"#{pad}class Client\n",
|
|
581
|
+
"#{pad} attr_reader :opts\n",
|
|
582
|
+
"\n",
|
|
583
|
+
"#{pad} def initialize(**kwargs)\n",
|
|
584
|
+
"#{pad} @opts = kwargs\n",
|
|
585
|
+
"#{pad} end\n",
|
|
586
|
+
"\n",
|
|
587
|
+
"#{pad} def connection(**override)\n",
|
|
588
|
+
"#{pad} Helpers::Client.connection(**@opts, **override)\n",
|
|
589
|
+
"#{pad} end\n",
|
|
590
|
+
"#{pad}end\n"
|
|
591
|
+
]
|
|
592
|
+
"# frozen_string_literal: true\n\n#{nested_module_wrap(inner)}"
|
|
515
593
|
end
|
|
516
594
|
|
|
517
595
|
def spec_helper_content
|
|
518
596
|
<<~RUBY
|
|
519
597
|
# frozen_string_literal: true
|
|
520
598
|
|
|
521
|
-
require '
|
|
599
|
+
require '#{require_path}'
|
|
522
600
|
|
|
523
601
|
RSpec.configure do |config|
|
|
524
602
|
config.expect_with :rspec do |expectations|
|
|
@@ -532,9 +610,9 @@ module Legion
|
|
|
532
610
|
<<~RUBY
|
|
533
611
|
# frozen_string_literal: true
|
|
534
612
|
|
|
535
|
-
RSpec.describe
|
|
613
|
+
RSpec.describe #{const_path} do
|
|
536
614
|
it 'has a version number' do
|
|
537
|
-
expect(
|
|
615
|
+
expect(#{const_path}::VERSION).not_to be_nil
|
|
538
616
|
end
|
|
539
617
|
end
|
|
540
618
|
RUBY
|
|
@@ -4,19 +4,62 @@ module Legion
|
|
|
4
4
|
module Extensions
|
|
5
5
|
module Helpers
|
|
6
6
|
module Base
|
|
7
|
+
# Words that mark the boundary between extension namespace segments and
|
|
8
|
+
# internal module structure. Segment extraction stops at these words.
|
|
9
|
+
NAMESPACE_BOUNDARIES = %w[Actor Actors Runners Helpers Transport Data].freeze
|
|
10
|
+
|
|
11
|
+
def segments
|
|
12
|
+
@segments ||= derive_segments_from_namespace
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def lex_slug
|
|
16
|
+
segments.join('.')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def log_tag
|
|
20
|
+
Helpers::Segments.segments_to_log_tag(segments)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def amqp_prefix
|
|
24
|
+
Helpers::Segments.segments_to_amqp_prefix(segments)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def settings_path
|
|
28
|
+
Helpers::Segments.segments_to_settings_path(segments)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def table_prefix
|
|
32
|
+
Helpers::Segments.segments_to_table_prefix(segments)
|
|
33
|
+
end
|
|
34
|
+
|
|
7
35
|
def lex_class
|
|
8
|
-
@lex_class ||=
|
|
36
|
+
@lex_class ||= begin
|
|
37
|
+
parts = calling_class_array
|
|
38
|
+
ext_idx = parts.index('Extensions')
|
|
39
|
+
# All LEX extensions must be under Legion::Extensions::. If 'Extensions'
|
|
40
|
+
# is not present, this is a misconfigured caller — fail loudly.
|
|
41
|
+
raise ArgumentError, "#{calling_class} is not under Legion::Extensions namespace" unless ext_idx
|
|
42
|
+
|
|
43
|
+
end_idx = ext_idx + 1
|
|
44
|
+
end_idx += 1 while end_idx < parts.length && !NAMESPACE_BOUNDARIES.include?(parts[end_idx])
|
|
45
|
+
# NameError cannot occur here: lex_class is only ever called from autobuild,
|
|
46
|
+
# build_transport, build_runners, build_actors, and transport helpers — all of
|
|
47
|
+
# which execute while the extension module is already required and fully defined.
|
|
48
|
+
# The constant we resolve (e.g. Legion::Extensions::Http) is the very module
|
|
49
|
+
# that owns this method, so it must already exist.
|
|
50
|
+
Kernel.const_get(parts[0...end_idx].join('::'))
|
|
51
|
+
end
|
|
9
52
|
end
|
|
10
53
|
alias extension_class lex_class
|
|
11
54
|
|
|
12
55
|
def lex_name
|
|
13
|
-
|
|
56
|
+
segments.join('_')
|
|
14
57
|
end
|
|
15
58
|
alias extension_name lex_name
|
|
16
59
|
alias lex_filename lex_name
|
|
17
60
|
|
|
18
61
|
def lex_const
|
|
19
|
-
@lex_const ||=
|
|
62
|
+
@lex_const ||= lex_class.to_s.split('::').last
|
|
20
63
|
end
|
|
21
64
|
|
|
22
65
|
def calling_class
|
|
@@ -52,7 +95,12 @@ module Legion
|
|
|
52
95
|
end
|
|
53
96
|
|
|
54
97
|
def full_path
|
|
55
|
-
@full_path ||=
|
|
98
|
+
@full_path ||= begin
|
|
99
|
+
gem_name = "lex-#{segments.join('-')}"
|
|
100
|
+
gem_dir = Gem::Specification.find_by_name(gem_name).gem_dir
|
|
101
|
+
require_path = Helpers::Segments.derive_require_path(gem_name)
|
|
102
|
+
"#{gem_dir}/lib/#{require_path}"
|
|
103
|
+
end
|
|
56
104
|
end
|
|
57
105
|
alias extension_path full_path
|
|
58
106
|
|
|
@@ -78,6 +126,28 @@ module Legion
|
|
|
78
126
|
end
|
|
79
127
|
end
|
|
80
128
|
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def derive_segments_from_namespace
|
|
133
|
+
parts = calling_class_array
|
|
134
|
+
ext_idx = parts.index('Extensions')
|
|
135
|
+
return [camelize_to_snake(parts[0])] unless ext_idx
|
|
136
|
+
|
|
137
|
+
ext_parts = []
|
|
138
|
+
((ext_idx + 1)...parts.length).each do |i|
|
|
139
|
+
break if NAMESPACE_BOUNDARIES.include?(parts[i])
|
|
140
|
+
|
|
141
|
+
ext_parts << camelize_to_snake(parts[i])
|
|
142
|
+
end
|
|
143
|
+
ext_parts.empty? ? [camelize_to_snake(parts[ext_idx + 1])] : ext_parts
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def camelize_to_snake(str)
|
|
147
|
+
str.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
148
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
149
|
+
.downcase
|
|
150
|
+
end
|
|
81
151
|
end
|
|
82
152
|
end
|
|
83
153
|
end
|
|
@@ -7,8 +7,11 @@ module Legion
|
|
|
7
7
|
def log
|
|
8
8
|
return @log unless @log.nil?
|
|
9
9
|
|
|
10
|
-
logger_hash =
|
|
11
|
-
|
|
10
|
+
logger_hash = if respond_to?(:segments)
|
|
11
|
+
{ lex_segments: Array(segments) }
|
|
12
|
+
else
|
|
13
|
+
{ lex: lex_filename.is_a?(Array) ? lex_filename.first : lex_filename }
|
|
14
|
+
end
|
|
12
15
|
if respond_to?(:settings) && settings.key?(:logger)
|
|
13
16
|
logger_hash[:level] = settings[:logger].key?(:level) ? settings[:logger][:level] : 'info'
|
|
14
17
|
logger_hash[:log_file] = settings[:logger][:log_file] if settings[:logger].key? :log_file
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Helpers
|
|
6
|
+
module Segments
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def derive_segments(gem_name)
|
|
10
|
+
gem_name.delete_prefix('lex-').split('-')
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def derive_namespace(gem_name)
|
|
14
|
+
derive_segments(gem_name).map { |s| s.split('_').map(&:capitalize).join }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def derive_const_path(gem_name)
|
|
18
|
+
"Legion::Extensions::#{derive_namespace(gem_name).join('::')}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def derive_require_path(gem_name)
|
|
22
|
+
"legion/extensions/#{derive_segments(gem_name).join('/')}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def segments_to_log_tag(segments)
|
|
26
|
+
segments.map { |s| "[#{s}]" }.join
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def segments_to_amqp_prefix(segments)
|
|
30
|
+
"legion.#{segments.join('.')}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def segments_to_settings_path(segments)
|
|
34
|
+
segments.map(&:to_sym)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def segments_to_table_prefix(segments)
|
|
38
|
+
segments.join('_')
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def categorize_gem(gem_name, categories:, lists:)
|
|
42
|
+
# Check defined lists first (list membership takes priority)
|
|
43
|
+
lists.each do |cat_name, gem_list|
|
|
44
|
+
next unless categories.key?(cat_name)
|
|
45
|
+
|
|
46
|
+
return { category: cat_name, tier: categories[cat_name][:tier] } if gem_list.include?(gem_name)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check prefix-matched categories
|
|
50
|
+
bare = gem_name.delete_prefix('lex-')
|
|
51
|
+
categories.each do |cat_name, cat_config|
|
|
52
|
+
next unless cat_config[:type] == :prefix
|
|
53
|
+
|
|
54
|
+
return { category: cat_name, tier: cat_config[:tier] } if bare.start_with?("#{cat_name}-")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
{ category: :default, tier: 5 }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -33,12 +33,13 @@ module Legion
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def build_default_exchange
|
|
36
|
-
|
|
37
|
-
return Object.const_get(exchange) if transport_class::Exchanges.const_defined? lex_const
|
|
36
|
+
return transport_class::Exchanges.const_get(lex_const) if transport_class::Exchanges.const_defined? lex_const
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
amqp = amqp_prefix
|
|
39
|
+
transport_class::Exchanges.const_set(lex_const, Class.new(Legion::Transport::Exchange) do
|
|
40
|
+
define_method(:exchange_name) { amqp }
|
|
41
|
+
end)
|
|
42
|
+
@default_exchange = transport_class::Exchanges.const_get(lex_const)
|
|
42
43
|
end
|
|
43
44
|
end
|
|
44
45
|
end
|
|
@@ -52,10 +52,9 @@ module Legion
|
|
|
52
52
|
end
|
|
53
53
|
return build_default_exchange if default_exchange
|
|
54
54
|
|
|
55
|
+
ext_amqp = amqp_prefix
|
|
55
56
|
transport_class::Exchanges.const_set(exchange.split('::').pop, Class.new(Legion::Transport::Exchange) do
|
|
56
|
-
|
|
57
|
-
self.class.ancestors.first.to_s.split('::')[5].downcase
|
|
58
|
-
end
|
|
57
|
+
define_method(:exchange_name) { "#{ext_amqp}.#{self.class.to_s.split('::').last.downcase}" }
|
|
59
58
|
end)
|
|
60
59
|
end
|
|
61
60
|
|
data/lib/legion/extensions.rb
CHANGED
|
@@ -39,23 +39,25 @@ module Legion
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def load_extensions
|
|
42
|
-
@extensions ||=
|
|
42
|
+
@extensions ||= []
|
|
43
43
|
@loaded_extensions ||= []
|
|
44
|
-
@extensions.each do |
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
@extensions.each do |entry|
|
|
45
|
+
gem_name = entry[:gem_name]
|
|
46
|
+
ext_name = entry[:require_path].split('/').last
|
|
47
|
+
|
|
48
|
+
if Legion::Settings[:extensions].key?(ext_name.to_sym) &&
|
|
49
|
+
Legion::Settings[:extensions][ext_name.to_sym].is_a?(Hash) &&
|
|
50
|
+
Legion::Settings[:extensions][ext_name.to_sym].key?(:enabled) &&
|
|
51
|
+
!Legion::Settings[:extensions][ext_name.to_sym][:enabled]
|
|
52
|
+
Legion::Logging.info "Skipping #{gem_name} because it's disabled"
|
|
47
53
|
next
|
|
48
54
|
end
|
|
49
55
|
|
|
50
|
-
|
|
56
|
+
unless load_extension(entry)
|
|
57
|
+
Legion::Logging.warn("#{gem_name} failed to load")
|
|
51
58
|
next
|
|
52
59
|
end
|
|
53
|
-
|
|
54
|
-
unless load_extension(extension, values)
|
|
55
|
-
Legion::Logging.warn("#{extension} failed to load")
|
|
56
|
-
next
|
|
57
|
-
end
|
|
58
|
-
@loaded_extensions.push(extension)
|
|
60
|
+
@loaded_extensions.push(gem_name)
|
|
59
61
|
end
|
|
60
62
|
Legion::Logging.info(
|
|
61
63
|
"#{@extensions.count} extensions loaded with " \
|
|
@@ -67,38 +69,47 @@ module Legion
|
|
|
67
69
|
)
|
|
68
70
|
end
|
|
69
71
|
|
|
70
|
-
def load_extension(
|
|
71
|
-
|
|
72
|
+
def load_extension(entry) # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/MethodLength
|
|
73
|
+
ensure_namespace(entry[:const_path]) if entry[:segments].length > 1
|
|
74
|
+
return unless gem_load(entry)
|
|
72
75
|
|
|
73
|
-
extension = Kernel.const_get(
|
|
76
|
+
extension = Kernel.const_get(entry[:const_path])
|
|
74
77
|
extension.extend Legion::Extensions::Core unless extension.singleton_class.include?(Legion::Extensions::Core)
|
|
75
78
|
|
|
76
|
-
|
|
79
|
+
ext_name = entry[:segments].join('_')
|
|
80
|
+
ext_settings = Legion::Settings[:extensions][ext_name.to_sym]
|
|
77
81
|
min_version = ext_settings[:min_version] if ext_settings.is_a?(Hash)
|
|
78
|
-
|
|
82
|
+
if min_version.is_a?(String)
|
|
83
|
+
begin
|
|
84
|
+
gem_spec = Gem::Specification.find_by_name(entry[:gem_name])
|
|
85
|
+
Legion::Logging.fatal entry if Gem::Version.new(gem_spec.version.to_s) >= Gem::Version.new(min_version)
|
|
86
|
+
rescue Gem::MissingSpecError
|
|
87
|
+
Legion::Logging.warn "Could not find gem spec for #{entry[:gem_name]}, skipping min_version check"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
79
90
|
|
|
80
91
|
if extension.data_required? && Legion::Settings[:data][:connected] == false
|
|
81
|
-
Legion::Logging.warn "#{
|
|
92
|
+
Legion::Logging.warn "#{ext_name} requires Legion::Data but isn't enabled, skipping"
|
|
82
93
|
return false
|
|
83
94
|
end
|
|
84
95
|
|
|
85
96
|
if extension.cache_required? && Legion::Settings[:cache][:connected] == false
|
|
86
|
-
Legion::Logging.warn "#{
|
|
97
|
+
Legion::Logging.warn "#{ext_name} requires Legion::Cache but isn't enabled, skipping"
|
|
87
98
|
return false
|
|
88
99
|
end
|
|
89
100
|
|
|
90
101
|
if extension.crypt_required? && Legion::Settings[:crypt][:cs].nil?
|
|
91
|
-
Legion::Logging.warn "#{
|
|
102
|
+
Legion::Logging.warn "#{ext_name} requires Legion::Crypt but isn't ready, skipping"
|
|
92
103
|
return false
|
|
93
104
|
end
|
|
94
105
|
|
|
95
106
|
if extension.vault_required? && Legion::Settings[:crypt][:vault][:connected] == false
|
|
96
|
-
Legion::Logging.warn "#{
|
|
107
|
+
Legion::Logging.warn "#{ext_name} requires Legion::Crypt::Vault but isn't enabled, skipping"
|
|
97
108
|
return false
|
|
98
109
|
end
|
|
99
110
|
|
|
100
111
|
if extension.llm_required? && (!Legion::Settings.key?(:llm) || Legion::Settings[:llm][:connected] == false)
|
|
101
|
-
Legion::Logging.warn "#{
|
|
112
|
+
Legion::Logging.warn "#{ext_name} requires Legion::LLM but isn't enabled, skipping"
|
|
102
113
|
return false
|
|
103
114
|
end
|
|
104
115
|
|
|
@@ -120,14 +131,14 @@ module Legion
|
|
|
120
131
|
hook_actor(**actor)
|
|
121
132
|
end
|
|
122
133
|
extension.log.info "Loaded v#{extension::VERSION}"
|
|
123
|
-
Legion::Events.emit('extension.loaded', name:
|
|
134
|
+
Legion::Events.emit('extension.loaded', name: ext_name, version: entry[:gem_name])
|
|
124
135
|
|
|
125
136
|
begin
|
|
126
137
|
if defined?(Legion::Data) && defined?(Legion::Data::Model::DigitalWorker)
|
|
127
|
-
worker_id = "lex-#{
|
|
138
|
+
worker_id = "lex-#{ext_name}"
|
|
128
139
|
worker = Legion::Data::Model::DigitalWorker.find_or_create(worker_id: worker_id) do |w|
|
|
129
|
-
w.name =
|
|
130
|
-
w.extension_name =
|
|
140
|
+
w.name = ext_name
|
|
141
|
+
w.extension_name = ext_name
|
|
131
142
|
w.lifecycle_state = 'active'
|
|
132
143
|
w.risk_tier = 'low'
|
|
133
144
|
w.team = 'extensions'
|
|
@@ -198,15 +209,27 @@ module Legion
|
|
|
198
209
|
end
|
|
199
210
|
end
|
|
200
211
|
|
|
201
|
-
def gem_load(
|
|
202
|
-
|
|
203
|
-
|
|
212
|
+
def gem_load(entry)
|
|
213
|
+
gem_name = entry[:gem_name]
|
|
214
|
+
require_path = entry[:require_path]
|
|
215
|
+
gem_dir = Gem::Specification.find_by_name(gem_name).gem_dir
|
|
216
|
+
require "#{gem_dir}/lib/#{require_path}"
|
|
204
217
|
true
|
|
218
|
+
rescue Gem::MissingSpecError => e
|
|
219
|
+
Legion::Logging.warn "#{gem_name} gem not found: #{e.message}"
|
|
220
|
+
nil
|
|
205
221
|
rescue LoadError => e
|
|
206
|
-
Legion::Logging.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
222
|
+
Legion::Logging.warn "#{gem_name} failed to load: #{e.message}"
|
|
223
|
+
nil
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def ensure_namespace(const_path)
|
|
227
|
+
parts = const_path.split('::')
|
|
228
|
+
current = ::Legion::Extensions
|
|
229
|
+
parts[2...-1].each do |part|
|
|
230
|
+
current.const_set(part, Module.new) unless current.const_defined?(part, false)
|
|
231
|
+
current = current.const_get(part, false)
|
|
232
|
+
end
|
|
210
233
|
end
|
|
211
234
|
|
|
212
235
|
def gem_names_for_discovery
|
|
@@ -222,17 +245,11 @@ module Legion
|
|
|
222
245
|
return if role.nil? || role[:profile].nil?
|
|
223
246
|
|
|
224
247
|
profile = role[:profile].to_sym
|
|
225
|
-
allowed =
|
|
226
|
-
|
|
227
|
-
when :cognitive then core_extension_names + agentic_extension_names
|
|
228
|
-
when :service then core_extension_names + service_extension_names + other_extension_names
|
|
229
|
-
when :dev then core_extension_names + ai_extension_names + dev_agentic_names
|
|
230
|
-
when :custom then Array(role[:extensions]).map(&:to_s)
|
|
231
|
-
else return
|
|
232
|
-
end
|
|
248
|
+
allowed = allowed_gem_names_for_profile(profile, role)
|
|
249
|
+
return if allowed.nil?
|
|
233
250
|
|
|
234
251
|
before = @extensions.count
|
|
235
|
-
@extensions.select! { |
|
|
252
|
+
@extensions.select! { |entry| allowed.include?(entry[:gem_name]) }
|
|
236
253
|
Legion::Logging.info "Role profile :#{profile} filtered #{before} -> #{@extensions.count} extensions"
|
|
237
254
|
end
|
|
238
255
|
|
|
@@ -260,61 +277,160 @@ module Legion
|
|
|
260
277
|
end
|
|
261
278
|
|
|
262
279
|
def agentic_extension_names
|
|
263
|
-
|
|
264
|
-
@extensions.
|
|
280
|
+
known_gem_names = (core_extension_names + service_extension_names + other_extension_names + ai_extension_names).map { |n| "lex-#{n}" }
|
|
281
|
+
Array(@extensions).reject { |entry| known_gem_names.include?(entry[:gem_name]) }.map { |entry| entry[:gem_name] }
|
|
265
282
|
end
|
|
266
283
|
|
|
267
|
-
def
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
284
|
+
def categorize_and_order(gem_names)
|
|
285
|
+
ext_settings = ::Legion::Settings[:extensions] || {}
|
|
286
|
+
categories = ext_settings[:categories] || default_category_registry
|
|
287
|
+
lists = {
|
|
288
|
+
core: Array(ext_settings[:core]),
|
|
289
|
+
ai: Array(ext_settings[:ai]),
|
|
290
|
+
gaia: Array(ext_settings[:gaia])
|
|
291
|
+
}
|
|
292
|
+
ctx = {
|
|
293
|
+
blocked: Array(ext_settings[:blocked]),
|
|
294
|
+
agentic_cfg: ext_settings[:agentic] || {},
|
|
295
|
+
categories: categories,
|
|
296
|
+
gem_set: gem_names.to_set,
|
|
297
|
+
ordered: [],
|
|
298
|
+
claimed: Set.new
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
collect_list_category_gems(lists, ctx)
|
|
302
|
+
collect_prefix_category_gems(gem_names, ctx)
|
|
303
|
+
|
|
304
|
+
(gem_names.to_a - ctx[:claimed].to_a - ctx[:blocked]).sort.each do |gn|
|
|
305
|
+
ctx[:ordered] << build_extension_entry(gn, :default, categories, nesting: false)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
ctx[:ordered]
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def check_reserved_words(gem_name, known_org: true)
|
|
312
|
+
return if known_org
|
|
313
|
+
|
|
314
|
+
bare = gem_name.delete_prefix('lex-')
|
|
315
|
+
first_segment = bare.split('-').first
|
|
316
|
+
|
|
317
|
+
configured_prefixes = begin
|
|
318
|
+
Array(::Legion::Settings.dig(:extensions, :reserved_prefixes))
|
|
319
|
+
rescue StandardError
|
|
320
|
+
[]
|
|
321
|
+
end
|
|
322
|
+
reserved_prefixes = configured_prefixes.empty? ? %w[core ai agentic gaia] : configured_prefixes
|
|
323
|
+
|
|
324
|
+
configured_words = begin
|
|
325
|
+
Array(::Legion::Settings.dig(:extensions, :reserved_words))
|
|
326
|
+
rescue StandardError
|
|
327
|
+
[]
|
|
328
|
+
end
|
|
329
|
+
reserved_words = configured_words.empty? ? %w[transport cache crypt data settings json logging llm rbac legion] : configured_words
|
|
330
|
+
|
|
331
|
+
if reserved_prefixes.include?(first_segment)
|
|
332
|
+
::Legion::Logging.warn(
|
|
333
|
+
"#{gem_name} uses reserved prefix '#{first_segment}' — " \
|
|
334
|
+
"it will be loaded in the #{first_segment} category namespace"
|
|
335
|
+
)
|
|
336
|
+
elsif reserved_words.include?(first_segment)
|
|
337
|
+
::Legion::Logging.warn(
|
|
338
|
+
"#{gem_name} uses reserved word '#{first_segment}' as its first segment — " \
|
|
339
|
+
'this may shadow framework modules'
|
|
340
|
+
)
|
|
278
341
|
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def find_extensions
|
|
345
|
+
return @extensions if @extensions
|
|
279
346
|
|
|
347
|
+
all_specs = gem_names_for_discovery
|
|
348
|
+
lex_names = all_specs.select { |s| s[:name].start_with?('lex-') }.map { |s| s[:name] }
|
|
349
|
+
@extensions = categorize_and_order(lex_names)
|
|
280
350
|
apply_role_filter
|
|
351
|
+
@extensions
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
private
|
|
355
|
+
|
|
356
|
+
def lex_prefix(names)
|
|
357
|
+
names.map { |n| n.start_with?('lex-') ? n : "lex-#{n}" }
|
|
358
|
+
end
|
|
281
359
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
requested += 1
|
|
290
|
-
next if values[:auto_install] == false
|
|
291
|
-
next if ENV['_'].include? 'bundle'
|
|
292
|
-
|
|
293
|
-
Legion::Logging.warn "#{extension} is missing, attempting to install automatically.."
|
|
294
|
-
install = Gem.install("lex-#{extension}", values[:version])
|
|
295
|
-
Legion::Logging.debug(install)
|
|
296
|
-
lex = Gem::Specification.find_by_name("lex-#{extension}")
|
|
297
|
-
|
|
298
|
-
@extensions[extension.to_s] = {
|
|
299
|
-
full_gem_name: "lex-#{extension}-#{lex.version}",
|
|
300
|
-
gem_name: "lex-#{extension}",
|
|
301
|
-
extension_name: extension.to_s,
|
|
302
|
-
version: lex.version,
|
|
303
|
-
extension_class: "Legion::Extensions::#{extension.to_s.split('_').collect(&:capitalize).join}"
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
enabled += 1
|
|
307
|
-
rescue StandardError, Gem::MissingSpecError => e
|
|
308
|
-
Legion::Logging.error "Failed to auto install #{extension}, e: #{e.message}"
|
|
360
|
+
def allowed_gem_names_for_profile(profile, role)
|
|
361
|
+
case profile
|
|
362
|
+
when :core then lex_prefix(core_extension_names)
|
|
363
|
+
when :cognitive then lex_prefix(core_extension_names + agentic_extension_names)
|
|
364
|
+
when :service then lex_prefix(core_extension_names + service_extension_names + other_extension_names)
|
|
365
|
+
when :dev then lex_prefix(core_extension_names + ai_extension_names + dev_agentic_names)
|
|
366
|
+
when :custom then lex_prefix(Array(role[:extensions]).map(&:to_s))
|
|
309
367
|
end
|
|
310
|
-
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def collect_list_category_gems(lists, ctx)
|
|
371
|
+
lists.sort_by { |cat, _| ctx[:categories].dig(cat, :tier) || 99 }.each do |cat_name, gem_list|
|
|
372
|
+
gem_list.each do |gn|
|
|
373
|
+
next unless ctx[:gem_set].include?(gn)
|
|
374
|
+
next if ctx[:blocked].include?(gn)
|
|
311
375
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
376
|
+
ctx[:ordered] << build_extension_entry(gn, cat_name, ctx[:categories], nesting: false)
|
|
377
|
+
ctx[:claimed].add(gn)
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def collect_prefix_category_gems(gem_names, ctx)
|
|
383
|
+
prefix_cats = ctx[:categories].select { |_, v| v[:type].to_s == 'prefix' }
|
|
384
|
+
.sort_by { |_, v| v[:tier] || 99 }
|
|
385
|
+
.to_h
|
|
386
|
+
prefix_cats.each_key do |cat_name|
|
|
387
|
+
prefix = "lex-#{cat_name}-"
|
|
388
|
+
matched = gem_names.select { |gn| gn.start_with?(prefix) && !ctx[:claimed].include?(gn) }.sort
|
|
389
|
+
matched.each do |gn|
|
|
390
|
+
next if ctx[:blocked].include?(gn)
|
|
391
|
+
next if cat_name == :agentic && agentic_blocked?(gn, ctx[:agentic_cfg])
|
|
392
|
+
next if cat_name == :agentic && !agentic_allowed?(gn, ctx[:agentic_cfg])
|
|
393
|
+
|
|
394
|
+
ctx[:ordered] << build_extension_entry(gn, cat_name, ctx[:categories], nesting: true)
|
|
395
|
+
ctx[:claimed].add(gn)
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def build_extension_entry(gem_name, category, categories, nesting:)
|
|
401
|
+
segments = Helpers::Segments.derive_segments(gem_name)
|
|
402
|
+
tier = category == :default ? 5 : (categories.dig(category, :tier) || 5)
|
|
403
|
+
|
|
404
|
+
if nesting
|
|
405
|
+
const_path = Helpers::Segments.derive_const_path(gem_name)
|
|
406
|
+
require_path = Helpers::Segments.derive_require_path(gem_name)
|
|
315
407
|
else
|
|
316
|
-
|
|
408
|
+
flat_name = gem_name.delete_prefix('lex-').tr('-', '_')
|
|
409
|
+
const_path = "Legion::Extensions::#{flat_name.split('_').map(&:capitalize).join}"
|
|
410
|
+
require_path = "legion/extensions/#{flat_name}"
|
|
317
411
|
end
|
|
412
|
+
|
|
413
|
+
{ gem_name: gem_name, category: category, tier: tier,
|
|
414
|
+
segments: segments, const_path: const_path, require_path: require_path }
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def default_category_registry
|
|
418
|
+
{
|
|
419
|
+
core: { type: :list, tier: 1 },
|
|
420
|
+
ai: { type: :list, tier: 2 },
|
|
421
|
+
gaia: { type: :list, tier: 3 },
|
|
422
|
+
agentic: { type: :prefix, tier: 4 }
|
|
423
|
+
}
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def agentic_blocked?(gem_name, config)
|
|
427
|
+
Array(config[:blocked]).any? { |pat| File.fnmatch(pat, gem_name) }
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def agentic_allowed?(gem_name, config)
|
|
431
|
+
return true if config[:allowed].nil?
|
|
432
|
+
|
|
433
|
+
Array(config[:allowed]).any? { |pat| File.fnmatch(pat, gem_name) }
|
|
318
434
|
end
|
|
319
435
|
end
|
|
320
436
|
end
|
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.4.
|
|
4
|
+
version: 1.4.58
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -514,6 +514,7 @@ files:
|
|
|
514
514
|
- lib/legion/extensions/helpers/data.rb
|
|
515
515
|
- lib/legion/extensions/helpers/lex.rb
|
|
516
516
|
- lib/legion/extensions/helpers/logger.rb
|
|
517
|
+
- lib/legion/extensions/helpers/segments.rb
|
|
517
518
|
- lib/legion/extensions/helpers/task.rb
|
|
518
519
|
- lib/legion/extensions/helpers/transport.rb
|
|
519
520
|
- lib/legion/extensions/hooks/base.rb
|