legionio 1.4.91 → 1.4.92

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.
Files changed (22) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/lib/legion/cli/lex/templates/data_pipeline/actors/ingest.rb.erb +24 -0
  4. data/lib/legion/cli/lex/templates/data_pipeline/runners/transform.rb.erb +56 -0
  5. data/lib/legion/cli/lex/templates/data_pipeline/spec/actors/ingest_spec.rb.erb +19 -0
  6. data/lib/legion/cli/lex/templates/data_pipeline/spec/runners/transform_spec.rb.erb +41 -0
  7. data/lib/legion/cli/lex/templates/data_pipeline/transport/exchanges/%name%.rb.erb +16 -0
  8. data/lib/legion/cli/lex/templates/data_pipeline/transport/messages/%name%_output.rb.erb +27 -0
  9. data/lib/legion/cli/lex/templates/data_pipeline/transport/queues/ingest.rb.erb +17 -0
  10. data/lib/legion/cli/lex/templates/llm_agent/helpers/client.rb.erb +43 -0
  11. data/lib/legion/cli/lex/templates/llm_agent/prompts/default.yml.erb +14 -0
  12. data/lib/legion/cli/lex/templates/llm_agent/runners/%name%.rb.erb +37 -0
  13. data/lib/legion/cli/lex/templates/llm_agent/spec/runners/%name%_spec.rb.erb +40 -0
  14. data/lib/legion/cli/lex/templates/service_integration/helpers/auth.rb.erb +37 -0
  15. data/lib/legion/cli/lex/templates/service_integration/helpers/client.rb.erb +53 -0
  16. data/lib/legion/cli/lex/templates/service_integration/runners/%name%.rb.erb +61 -0
  17. data/lib/legion/cli/lex/templates/service_integration/spec/helpers/client_spec.rb.erb +46 -0
  18. data/lib/legion/cli/lex/templates/service_integration/spec/runners/%name%_spec.rb.erb +52 -0
  19. data/lib/legion/cli/lex_command.rb +55 -9
  20. data/lib/legion/cli/lex_templates.rb +74 -2
  21. data/lib/legion/version.rb +1 -1
  22. metadata +17 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e39fb298ff77ee509f8a7948c14455a48b4764503688ead869e48ea4108e72a0
4
- data.tar.gz: 0266b0b3d36ecf72e7b2be8d2f0f42ae9737fd0a37add807b32a608dd7fcf5c0
3
+ metadata.gz: 0d9e1879209235490bdbd660fc1d57fcc57d9487b4a17b96fce351311682fabd
4
+ data.tar.gz: 05e9a1fea38e182e9d2d33df3decc365f6af12d5f1a58e7496e33dfdfd5431e4
5
5
  SHA512:
6
- metadata.gz: 5edbd698137c046896280b44cd8af35580808068a24ee48a95dd495e48f57df5eb0976865e6ca8f60938bf96e0d3a969a0085f6ba18a93b8045c34f41958f119
7
- data.tar.gz: 3048fe946eaade9f1e3ecb611d4eced9e78bb5e80126d09e73a55b5e7aa017df594e75f2e17fc3b88af87d9e1d960ee5c988dc0e7022d4260a2fa60ba3645b06
6
+ metadata.gz: 2d56827c6a9bd485fb2a1e72e4b9dcd651074c1222016e3f369eee43d22d580d3c7a369bb0a07d9f47e21c453b476bf7132ec217834c3fb06fefcc1f82079b7a
7
+ data.tar.gz: 72c7bf18dc56f721e290a576ac71798b9559e5a91aeab6db2a720d8b5e97125e2909d62f1489dd7aea2806009b84392db5ab7cac32618e7b7b24ccc53677bfa3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Legion Changelog
2
2
 
3
+ ## [1.4.92] - 2026-03-20
4
+
5
+ ### Added
6
+ - `--template` option on `legion lex create` to scaffold pattern-specific extensions: `llm-agent`, `service-integration`, `data-pipeline` (default: `basic`)
7
+ - `--list-templates` option on `legion lex create` to display available templates with descriptions
8
+ - `LexTemplates::TemplateOverlay` class renders ERB template files into the target extension directory
9
+ - ERB scaffold templates under `lib/legion/cli/lex/templates/`: `llm_agent/`, `service_integration/`, `data_pipeline/`
10
+ - `llm-agent` template: LLM runner with `Legion::LLM.chat` and structured output, helpers/client.rb with model/temperature kwargs, default prompt YAML, spec with LLM mock
11
+ - `service-integration` template: CRUD runners (list/get/create/update/delete), Faraday HTTP client helper with api_key/bearer/basic auth, auth helper, specs with WebMock stubs
12
+ - `data-pipeline` template: transform runner with validate/process/publish pattern, subscription ingest actor, transport exchange/queue/message scaffolds, runner and actor specs
13
+ - Template registry extended with `data-pipeline`, `template_dir` class method, new `llm-agent`/`service-integration` entries with `template_dir` keys
14
+
3
15
  ## [1.4.91] - 2026-03-20
4
16
 
5
17
  ### Fixed
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module <%= lex_class %>
6
+ module Actors
7
+ class Ingest < Legion::Extensions::Actors::Subscription
8
+ include Legion::Extensions::<%= lex_class %>::Runners::Transform
9
+
10
+ QUEUE = 'legion.<%= lex_name %>.ingest'
11
+ EXCHANGE = 'legion.<%= lex_name %>'
12
+
13
+ def self.queue
14
+ QUEUE
15
+ end
16
+
17
+ def self.exchange
18
+ EXCHANGE
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module <%= lex_class %>
6
+ module Runners
7
+ module Transform
8
+ extend Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?('Helpers::Lex')
9
+
10
+ # Main transform entry point. Receives a payload hash, returns transformed output.
11
+ def transform(payload:, options: {}, **)
12
+ validated = validate_input(payload)
13
+ return validated unless validated[:success]
14
+
15
+ result = process(validated[:data], options)
16
+ publish_output(result) if defined?(Legion::Transport)
17
+
18
+ { success: true, data: result }
19
+ rescue StandardError => e
20
+ handle_error(e, payload)
21
+ end
22
+
23
+ private
24
+
25
+ def validate_input(payload)
26
+ return { success: false, reason: 'payload is required' } if payload.nil?
27
+ return { success: false, reason: 'payload must be a Hash' } unless payload.is_a?(Hash)
28
+
29
+ { success: true, data: payload }
30
+ end
31
+
32
+ def process(data, _options)
33
+ # TODO: implement transformation logic
34
+ data
35
+ end
36
+
37
+ def publish_output(result)
38
+ return unless defined?(Legion::Transport)
39
+
40
+ Legion::Transport::Messages::<%= name_class %>Output.new(data: result).publish
41
+ rescue StandardError
42
+ nil
43
+ end
44
+
45
+ def handle_error(error, payload)
46
+ {
47
+ success: false,
48
+ reason: error.message,
49
+ payload: payload
50
+ }
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::<%= lex_class %>::Actors::Ingest do
4
+ it 'inherits from Subscription actor' do
5
+ expect(described_class.ancestors).to include(Legion::Extensions::Actors::Subscription)
6
+ end
7
+
8
+ it 'includes the Transform runner' do
9
+ expect(described_class.ancestors).to include(Legion::Extensions::<%= lex_class %>::Runners::Transform)
10
+ end
11
+
12
+ it 'defines a queue name' do
13
+ expect(described_class::QUEUE).to include('<%= lex_name %>')
14
+ end
15
+
16
+ it 'defines an exchange name' do
17
+ expect(described_class::EXCHANGE).to include('<%= lex_name %>')
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::<%= lex_class %>::Runners::Transform do
4
+ subject { described_class }
5
+
6
+ let(:test_class) do
7
+ Class.new do
8
+ extend Legion::Extensions::<%= lex_class %>::Runners::Transform
9
+ end
10
+ end
11
+
12
+ it { should be_a Module }
13
+ it { is_expected.to respond_to(:transform).with_any_keywords }
14
+
15
+ describe '#transform' do
16
+ it 'returns success for a valid payload' do
17
+ result = test_class.transform(payload: { key: 'value' })
18
+ expect(result[:success]).to be true
19
+ expect(result[:data]).to be_a(Hash)
20
+ end
21
+
22
+ it 'returns failure when payload is nil' do
23
+ result = test_class.transform(payload: nil)
24
+ expect(result[:success]).to be false
25
+ expect(result[:reason]).to include('required')
26
+ end
27
+
28
+ it 'returns failure when payload is not a Hash' do
29
+ result = test_class.transform(payload: 'not a hash')
30
+ expect(result[:success]).to be false
31
+ expect(result[:reason]).to include('Hash')
32
+ end
33
+
34
+ it 'handles unexpected errors gracefully' do
35
+ allow(test_class).to receive(:process).and_raise(StandardError, 'unexpected')
36
+ result = test_class.transform(payload: { key: 'value' })
37
+ expect(result[:success]).to be false
38
+ expect(result[:reason]).to eq('unexpected')
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module <%= lex_class %>
6
+ module Transport
7
+ module Exchanges
8
+ class <%= name_class %> < Legion::Transport::Exchange
9
+ EXCHANGE_NAME = 'legion.<%= lex_name %>'
10
+ EXCHANGE_TYPE = :direct
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module <%= lex_class %>
6
+ module Transport
7
+ module Messages
8
+ class <%= name_class %>Output < Legion::Transport::Message
9
+ EXCHANGE_NAME = 'legion.<%= lex_name %>'
10
+ ROUTING_KEY = 'output'
11
+
12
+ attr_accessor :data
13
+
14
+ def initialize(data:)
15
+ @data = data
16
+ super()
17
+ end
18
+
19
+ def to_payload
20
+ { data: @data }
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module <%= lex_class %>
6
+ module Transport
7
+ module Queues
8
+ class Ingest < Legion::Transport::Queue
9
+ QUEUE_NAME = 'legion.<%= lex_name %>.ingest'
10
+ EXCHANGE_NAME = 'legion.<%= lex_name %>'
11
+ ROUTING_KEY = 'ingest'
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module <%= lex_class %>
6
+ module Helpers
7
+ class Client
8
+ attr_reader :model, :temperature, :max_tokens
9
+
10
+ def initialize(model: nil, temperature: 0.7, max_tokens: 1024, **)
11
+ @model = model
12
+ @temperature = temperature
13
+ @max_tokens = max_tokens
14
+ end
15
+
16
+ def chat(prompt:, **override)
17
+ return { success: false, reason: 'legion-llm not available' } unless defined?(Legion::LLM)
18
+
19
+ opts = { prompt: prompt, temperature: @temperature, max_tokens: @max_tokens }
20
+ opts[:model] = @model if @model
21
+ opts.merge!(override)
22
+
23
+ Legion::LLM.chat(**opts)
24
+ rescue StandardError => e
25
+ { success: false, reason: e.message }
26
+ end
27
+
28
+ def structured(prompt:, schema:, **override)
29
+ return { success: false, reason: 'legion-llm not available' } unless defined?(Legion::LLM)
30
+
31
+ opts = { prompt: prompt, schema: schema }
32
+ opts[:model] = @model if @model
33
+ opts.merge!(override)
34
+
35
+ Legion::LLM.structured(**opts)
36
+ rescue StandardError => e
37
+ { success: false, reason: e.message }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,14 @@
1
+ ---
2
+ # Default prompt template for <%= gem_name %>
3
+ # Customize system and user prompts for your use case.
4
+
5
+ system: |
6
+ You are a helpful assistant for <%= lex_class %>.
7
+ Respond concisely and accurately.
8
+
9
+ user: |
10
+ <%= '{{input}}' %>
11
+
12
+ examples:
13
+ - input: "What can you help me with?"
14
+ expected: "I can help you with <%= lex_class.downcase %>-related tasks."
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module <%= lex_class %>
6
+ module Runners
7
+ module <%= name_class %>
8
+ extend Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?('Helpers::Lex')
9
+
10
+ def run(prompt:, model: nil, temperature: nil, structured: false, schema: nil, **)
11
+ llm_opts = { prompt: prompt }
12
+ llm_opts[:model] = model if model
13
+ llm_opts[:temperature] = temperature if temperature
14
+
15
+ if structured && defined?(Legion::LLM)
16
+ response = Legion::LLM.structured(prompt: prompt, schema: schema || default_schema)
17
+ elsif defined?(Legion::LLM)
18
+ response = Legion::LLM.chat(**llm_opts)
19
+ else
20
+ return { success: false, reason: 'legion-llm not available' }
21
+ end
22
+
23
+ { success: true, response: response }
24
+ rescue StandardError => e
25
+ { success: false, reason: e.message }
26
+ end
27
+
28
+ private
29
+
30
+ def default_schema
31
+ { type: 'object', properties: { result: { type: 'string' } } }
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::<%= lex_class %>::Runners::<%= name_class %> do
4
+ subject { described_class }
5
+
6
+ let(:test_class) do
7
+ Class.new do
8
+ extend Legion::Extensions::<%= lex_class %>::Runners::<%= name_class %>
9
+ end
10
+ end
11
+
12
+ before do
13
+ stub_const('Legion::LLM', Module.new do
14
+ def self.chat(**_opts)
15
+ { success: true, content: 'mock response' }
16
+ end
17
+
18
+ def self.structured(**_opts)
19
+ { success: true, result: 'mock result' }
20
+ end
21
+ end)
22
+ end
23
+
24
+ it { should be_a Module }
25
+ it { is_expected.to respond_to(:run).with_any_keywords }
26
+
27
+ describe '#run' do
28
+ it 'returns success with a response' do
29
+ result = test_class.run(prompt: 'hello')
30
+ expect(result[:success]).to be true
31
+ end
32
+
33
+ it 'returns failure when legion-llm is unavailable' do
34
+ hide_const('Legion::LLM')
35
+ result = test_class.run(prompt: 'hello')
36
+ expect(result[:success]).to be false
37
+ expect(result[:reason]).to include('legion-llm')
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module <%= lex_class %>
6
+ module Helpers
7
+ module Auth
8
+ # Build an auth hash from settings or explicit kwargs.
9
+ # Supports three methods:
10
+ # api_key — { method: :api_key, key: '...', header: 'X-API-Key' }
11
+ # bearer — { method: :bearer, token: '...' }
12
+ # basic — { method: :basic, username: '...', password: '...' }
13
+ def self.from_settings(settings = {})
14
+ return {} if settings.nil? || settings.empty?
15
+
16
+ auth = settings[:auth] || settings['auth'] || {}
17
+ return {} if auth.empty?
18
+
19
+ auth
20
+ end
21
+
22
+ def self.api_key(key, header: 'X-API-Key')
23
+ { method: :api_key, key: key, header: header }
24
+ end
25
+
26
+ def self.bearer(token)
27
+ { method: :bearer, token: token }
28
+ end
29
+
30
+ def self.basic(username, password)
31
+ { method: :basic, username: username, password: password }
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'json'
5
+
6
+ module Legion
7
+ module Extensions
8
+ module <%= lex_class %>
9
+ module Helpers
10
+ class Client
11
+ DEFAULT_TIMEOUT = 30
12
+
13
+ class << self
14
+ def connection(base_url: nil, timeout: DEFAULT_TIMEOUT, auth: {}, **)
15
+ raise ArgumentError, 'base_url is required' if base_url.nil? || base_url.empty?
16
+
17
+ Faraday.new(url: base_url) do |conn|
18
+ conn.options.timeout = timeout
19
+ conn.options.open_timeout = timeout
20
+ conn.headers['Content-Type'] = 'application/json'
21
+ conn.headers['Accept'] = 'application/json'
22
+
23
+ apply_auth(conn, auth)
24
+
25
+ conn.response :json, content_type: /\bjson$/
26
+ conn.adapter Faraday.default_adapter
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def apply_auth(conn, auth)
33
+ method = auth[:method] || auth['method']
34
+
35
+ case method&.to_sym
36
+ when :api_key
37
+ header = auth[:header] || auth['header'] || 'X-API-Key'
38
+ conn.headers[header] = auth[:key] || auth['key']
39
+ when :bearer
40
+ token = auth[:token] || auth['token']
41
+ conn.headers['Authorization'] = "Bearer #{token}"
42
+ when :basic
43
+ conn.request :authorization, :basic,
44
+ auth[:username] || auth['username'],
45
+ auth[:password] || auth['password']
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module <%= lex_class %>
6
+ module Runners
7
+ module <%= name_class %>
8
+ extend Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?('Helpers::Lex')
9
+
10
+ def list(**opts)
11
+ client = Helpers::Client.connection(**settings.merge(opts))
12
+ response = client.get('/')
13
+ { success: true, data: response.body }
14
+ rescue StandardError => e
15
+ { success: false, reason: e.message }
16
+ end
17
+
18
+ def get(id:, **)
19
+ client = Helpers::Client.connection(**settings)
20
+ response = client.get("/#{id}")
21
+ { success: true, data: response.body }
22
+ rescue StandardError => e
23
+ { success: false, reason: e.message }
24
+ end
25
+
26
+ def create(**payload)
27
+ client = Helpers::Client.connection(**settings)
28
+ response = client.post('/') { |req| req.body = payload.to_json }
29
+ { success: true, data: response.body }
30
+ rescue StandardError => e
31
+ { success: false, reason: e.message }
32
+ end
33
+
34
+ def update(id:, **payload)
35
+ client = Helpers::Client.connection(**settings)
36
+ response = client.put("/#{id}") { |req| req.body = payload.to_json }
37
+ { success: true, data: response.body }
38
+ rescue StandardError => e
39
+ { success: false, reason: e.message }
40
+ end
41
+
42
+ def delete(id:, **)
43
+ client = Helpers::Client.connection(**settings)
44
+ response = client.delete("/#{id}")
45
+ { success: true, status: response.status }
46
+ rescue StandardError => e
47
+ { success: false, reason: e.message }
48
+ end
49
+
50
+ private
51
+
52
+ def settings
53
+ return {} unless defined?(Legion::Settings)
54
+
55
+ Legion::Settings.dig(:extensions, :<%= lex_name %>) || {}
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'webmock/rspec'
4
+
5
+ RSpec.describe Legion::Extensions::<%= lex_class %>::Helpers::Client do
6
+ let(:base_url) { 'https://api.example.com' }
7
+
8
+ before { WebMock.enable! }
9
+ after { WebMock.disable! }
10
+
11
+ describe '.connection' do
12
+ it 'raises ArgumentError when base_url is missing' do
13
+ expect { described_class.connection }.to raise_error(ArgumentError, /base_url/)
14
+ end
15
+
16
+ it 'returns a Faraday connection' do
17
+ conn = described_class.connection(base_url: base_url)
18
+ expect(conn).to be_a(Faraday::Connection)
19
+ end
20
+
21
+ it 'sets Content-Type header to application/json' do
22
+ conn = described_class.connection(base_url: base_url)
23
+ expect(conn.headers['Content-Type']).to eq('application/json')
24
+ end
25
+
26
+ context 'with api_key auth' do
27
+ it 'sets the API key header' do
28
+ conn = described_class.connection(
29
+ base_url: base_url,
30
+ auth: { method: :api_key, key: 'secret', header: 'X-API-Key' }
31
+ )
32
+ expect(conn.headers['X-API-Key']).to eq('secret')
33
+ end
34
+ end
35
+
36
+ context 'with bearer auth' do
37
+ it 'sets Authorization header' do
38
+ conn = described_class.connection(
39
+ base_url: base_url,
40
+ auth: { method: :bearer, token: 'mytoken' }
41
+ )
42
+ expect(conn.headers['Authorization']).to eq('Bearer mytoken')
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'webmock/rspec'
4
+
5
+ RSpec.describe Legion::Extensions::<%= lex_class %>::Runners::<%= name_class %> do
6
+ subject { described_class }
7
+
8
+ let(:base_url) { 'https://api.example.com' }
9
+ let(:test_class) do
10
+ Class.new do
11
+ extend Legion::Extensions::<%= lex_class %>::Runners::<%= name_class %>
12
+
13
+ def self.settings
14
+ { base_url: 'https://api.example.com' }
15
+ end
16
+ end
17
+ end
18
+
19
+ before { WebMock.enable! }
20
+ after { WebMock.disable! }
21
+
22
+ it { should be_a Module }
23
+ it { is_expected.to respond_to(:list).with_any_keywords }
24
+ it { is_expected.to respond_to(:get).with_any_keywords }
25
+ it { is_expected.to respond_to(:create).with_any_keywords }
26
+ it { is_expected.to respond_to(:update).with_any_keywords }
27
+ it { is_expected.to respond_to(:delete).with_any_keywords }
28
+
29
+ describe '#list' do
30
+ before do
31
+ stub_request(:get, "#{base_url}/")
32
+ .to_return(status: 200, body: '[]', headers: { 'Content-Type' => 'application/json' })
33
+ end
34
+
35
+ it 'returns success' do
36
+ result = test_class.list
37
+ expect(result[:success]).to be true
38
+ end
39
+ end
40
+
41
+ describe '#get' do
42
+ before do
43
+ stub_request(:get, "#{base_url}/42")
44
+ .to_return(status: 200, body: '{"id":42}', headers: { 'Content-Type' => 'application/json' })
45
+ end
46
+
47
+ it 'returns success with data' do
48
+ result = test_class.get(id: 42)
49
+ expect(result[:success]).to be true
50
+ end
51
+ end
52
+ end
@@ -3,6 +3,7 @@
3
3
  require 'fileutils'
4
4
  require 'legion/extensions/helpers/segments'
5
5
  require 'legion/cli/lex_cli_manifest'
6
+ require 'legion/cli/lex_templates'
6
7
 
7
8
  module Legion
8
9
  module CLI
@@ -96,14 +97,34 @@ module Legion
96
97
  method_option :bundle_install, type: :boolean, default: true, desc: 'Run bundle install'
97
98
  method_option :category, type: :string, default: nil,
98
99
  desc: 'Extension category (agentic, ai, gaia). Determines namespace nesting and gem prefix.'
99
- def create(name)
100
+ method_option :template, type: :string, default: 'basic',
101
+ desc: 'Scaffold template: basic, llm-agent, service-integration, data-pipeline'
102
+ method_option :list_templates, type: :boolean, default: false,
103
+ desc: 'List available scaffold templates and exit'
104
+ def create(name = nil)
100
105
  out = formatter
101
106
 
107
+ if options[:list_templates]
108
+ render_template_list(out)
109
+ return
110
+ end
111
+
112
+ unless name
113
+ out.error('NAME is required. Usage: legion lex create NAME [--template TEMPLATE]')
114
+ return
115
+ end
116
+
102
117
  if options[:category] && options[:category] !~ /\A[a-z][a-z0-9_-]*\z/
103
118
  out.error('--category must be lowercase letters, numbers, underscores, or hyphens')
104
119
  return
105
120
  end
106
121
 
122
+ template_name = options[:template] || 'basic'
123
+ unless LexTemplates.valid?(template_name)
124
+ out.warn("Unknown template '#{template_name}', falling back to 'basic'. Run `legion lex create --list-templates` to see available templates.")
125
+ template_name = 'basic'
126
+ end
127
+
107
128
  gem_name = options[:category] ? "lex-#{options[:category]}-#{name}" : "lex-#{name}"
108
129
  target_dir = gem_name
109
130
 
@@ -119,11 +140,11 @@ module Legion
119
140
 
120
141
  Legion::Extensions.check_reserved_words(gem_name, known_org: false)
121
142
 
122
- out.success("Creating #{gem_name}...")
143
+ out.success("Creating #{gem_name} (template: #{template_name})...")
123
144
 
124
145
  vars = { filename: target_dir, class_name: name.split('_').map(&:capitalize).join, lex: name }
125
146
 
126
- generator = LexGenerator.new(name, vars, options, gem_name: gem_name)
147
+ generator = LexGenerator.new(name, vars, options, gem_name: gem_name, template: template_name)
127
148
  generator.generate(out)
128
149
 
129
150
  out.spacer
@@ -267,6 +288,17 @@ module Legion
267
288
  )
268
289
  end
269
290
 
291
+ def render_template_list(out)
292
+ templates = LexTemplates.list
293
+ if options[:json]
294
+ out.json(templates)
295
+ else
296
+ out.header('Available scaffold templates')
297
+ rows = templates.map { |t| [t[:name], t[:description]] }
298
+ out.table(%w[template description], rows)
299
+ end
300
+ end
301
+
270
302
  def render_flat_table(out, rows)
271
303
  table_rows = rows.map do |l|
272
304
  [l[:name], l[:version], l[:category].to_s, l[:tier].to_s, out.status(l[:status]), l[:runners].to_s, l[:actors].to_s]
@@ -399,16 +431,18 @@ module Legion
399
431
 
400
432
  # Thin generator class that wraps the template logic
401
433
  class LexGenerator
402
- def initialize(name, vars, options, gem_name: nil)
403
- @name = name
404
- @vars = vars
405
- @options = options
406
- @gem_name = gem_name || "lex-#{name}"
407
- @target = @gem_name
434
+ def initialize(name, vars, options, gem_name: nil, template: 'basic')
435
+ @name = name
436
+ @vars = vars
437
+ @options = options
438
+ @gem_name = gem_name || "lex-#{name}"
439
+ @target = @gem_name
440
+ @template = template || 'basic'
408
441
  end
409
442
 
410
443
  def generate(out)
411
444
  create_structure(out)
445
+ apply_template_overlay(out) unless @template == 'basic'
412
446
  init_git(out) if @options[:git_init]
413
447
  run_bundle(out) if @options[:bundle_install]
414
448
  end
@@ -515,6 +549,18 @@ module Legion
515
549
  File.write(path, content)
516
550
  end
517
551
 
552
+ def apply_template_overlay(out)
553
+ segs = Legion::Extensions::Helpers::Segments.derive_segments(@gem_name)
554
+ lex_class = segs.map(&:capitalize).join('::')
555
+ lex_name = @name
556
+ name_class = @name.split(/[_-]/).map(&:capitalize).join
557
+ gem_name = @gem_name
558
+
559
+ vars = { lex_class: lex_class, lex_name: lex_name, name_class: name_class, gem_name: gem_name }
560
+ overlay = LexTemplates::TemplateOverlay.new(@template, @target, vars)
561
+ overlay.apply(out)
562
+ end
563
+
518
564
  def init_git(out)
519
565
  Dir.chdir(@target) do
520
566
  system('git init -q')
@@ -1,8 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'erb'
4
+ require 'fileutils'
5
+
3
6
  module Legion
4
7
  module CLI
5
8
  module LexTemplates
9
+ TEMPLATES_DIR = File.join(File.dirname(__FILE__), 'lex', 'templates').freeze
10
+
6
11
  REGISTRY = {
7
12
  'basic' => {
8
13
  runners: ['default'],
@@ -18,7 +23,8 @@ module Legion
18
23
  tools: %w[process analyze],
19
24
  client: true,
20
25
  dependencies: ['legion-llm'],
21
- description: 'LLM-powered agent extension'
26
+ description: 'LLM-powered agent extension',
27
+ template_dir: 'llm_agent'
22
28
  },
23
29
  'service-integration' => {
24
30
  runners: ['operations'],
@@ -26,7 +32,17 @@ module Legion
26
32
  tools: [],
27
33
  client: true,
28
34
  dependencies: [],
29
- description: 'External service integration with standalone client'
35
+ description: 'External service integration with standalone client',
36
+ template_dir: 'service_integration'
37
+ },
38
+ 'data-pipeline' => {
39
+ runners: ['transform'],
40
+ actors: ['ingest'],
41
+ tools: [],
42
+ client: false,
43
+ dependencies: [],
44
+ description: 'Event-driven data processing pipeline',
45
+ template_dir: 'data_pipeline'
30
46
  },
31
47
  'scheduled-task' => {
32
48
  runners: ['executor'],
@@ -58,6 +74,62 @@ module Legion
58
74
  def valid?(name)
59
75
  REGISTRY.key?(name.to_s)
60
76
  end
77
+
78
+ def template_dir(name)
79
+ config = REGISTRY[name.to_s]
80
+ return nil unless config
81
+
82
+ dir_key = config[:template_dir]
83
+ return nil unless dir_key
84
+
85
+ File.join(TEMPLATES_DIR, dir_key)
86
+ end
87
+ end
88
+
89
+ # Renders and writes template-specific overlay files into the target extension directory.
90
+ class TemplateOverlay
91
+ PLACEHOLDER = '%name%'
92
+
93
+ # vars: { gem_name:, lex_name:, lex_class:, name_class: }
94
+ def initialize(template_name, target_dir, vars)
95
+ @template_name = template_name
96
+ @target_dir = target_dir
97
+ @vars = vars
98
+ end
99
+
100
+ def apply(out = nil)
101
+ src = LexTemplates.template_dir(@template_name)
102
+ return unless src && Dir.exist?(src)
103
+
104
+ each_template_file(src) do |abs_src, rel_path|
105
+ dest_rel = rel_path.gsub(PLACEHOLDER, @vars[:lex_name])
106
+ dest_rel = dest_rel.sub(/\.erb$/, '')
107
+ dest_abs = File.join(@target_dir, dest_rel)
108
+
109
+ FileUtils.mkdir_p(File.dirname(dest_abs))
110
+ rendered = render_erb(File.read(abs_src))
111
+ File.write(dest_abs, rendered)
112
+ out&.success(" [#{@template_name}] #{dest_rel}")
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ def each_template_file(src_dir, &block)
119
+ Dir.glob("#{src_dir}/**/*.erb").each do |abs_src|
120
+ rel_path = abs_src.sub("#{src_dir}/", '')
121
+ block.call(abs_src, rel_path)
122
+ end
123
+ end
124
+
125
+ def render_erb(template_text)
126
+ lex_class = @vars[:lex_class]
127
+ lex_name = @vars[:lex_name]
128
+ name_class = @vars[:name_class]
129
+ gem_name = @vars[:gem_name]
130
+
131
+ ERB.new(template_text, trim_mode: '-').result(binding)
132
+ end
61
133
  end
62
134
  end
63
135
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.4.91'
4
+ VERSION = '1.4.92'
5
5
  end
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.91
4
+ version: 1.4.92
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -494,8 +494,19 @@ files:
494
494
  - lib/legion/cli/lex/templates/base/rubocop.yml.erb
495
495
  - lib/legion/cli/lex/templates/base/spec_helper.rb.erb
496
496
  - lib/legion/cli/lex/templates/base/version.erb
497
+ - lib/legion/cli/lex/templates/data_pipeline/actors/ingest.rb.erb
498
+ - lib/legion/cli/lex/templates/data_pipeline/runners/transform.rb.erb
499
+ - lib/legion/cli/lex/templates/data_pipeline/spec/actors/ingest_spec.rb.erb
500
+ - lib/legion/cli/lex/templates/data_pipeline/spec/runners/transform_spec.rb.erb
501
+ - lib/legion/cli/lex/templates/data_pipeline/transport/exchanges/%name%.rb.erb
502
+ - lib/legion/cli/lex/templates/data_pipeline/transport/messages/%name%_output.rb.erb
503
+ - lib/legion/cli/lex/templates/data_pipeline/transport/queues/ingest.rb.erb
497
504
  - lib/legion/cli/lex/templates/exchange.erb
498
505
  - lib/legion/cli/lex/templates/exchange_spec.erb
506
+ - lib/legion/cli/lex/templates/llm_agent/helpers/client.rb.erb
507
+ - lib/legion/cli/lex/templates/llm_agent/prompts/default.yml.erb
508
+ - lib/legion/cli/lex/templates/llm_agent/runners/%name%.rb.erb
509
+ - lib/legion/cli/lex/templates/llm_agent/spec/runners/%name%_spec.rb.erb
499
510
  - lib/legion/cli/lex/templates/message.erb
500
511
  - lib/legion/cli/lex/templates/message_spec.erb
501
512
  - lib/legion/cli/lex/templates/queue.erb
@@ -503,6 +514,11 @@ files:
503
514
  - lib/legion/cli/lex/templates/queue_spec.erb
504
515
  - lib/legion/cli/lex/templates/runner.erb
505
516
  - lib/legion/cli/lex/templates/runner_spec.erb
517
+ - lib/legion/cli/lex/templates/service_integration/helpers/auth.rb.erb
518
+ - lib/legion/cli/lex/templates/service_integration/helpers/client.rb.erb
519
+ - lib/legion/cli/lex/templates/service_integration/runners/%name%.rb.erb
520
+ - lib/legion/cli/lex/templates/service_integration/spec/helpers/client_spec.rb.erb
521
+ - lib/legion/cli/lex/templates/service_integration/spec/runners/%name%_spec.rb.erb
506
522
  - lib/legion/cli/lex_cli_manifest.rb
507
523
  - lib/legion/cli/lex_command.rb
508
524
  - lib/legion/cli/lex_templates.rb