legionio 1.4.82 → 1.4.85

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be7718191541e7f3c7d122c0df0f74145ef0ed5aa8e4b8a67396932155e4cccf
4
- data.tar.gz: 5b4f4e5453fe9667b6424433d8d0ec9cfe524b6f9c35be64f7e4b0edeae1c3f3
3
+ metadata.gz: 512139af5d950a28790627a762f8dc5f52e37cd093ed2829a9a42aa833ededec
4
+ data.tar.gz: f80a9bfa077caa791bf68058c91eb1061b9cf3851c1766ef5aedd49e9ecda95e
5
5
  SHA512:
6
- metadata.gz: dd10e410a69d1cae47dadbaf02f287857410b44e8f8d305c1cd8b555a44a32d2a6b0a63950a3ee2e54d815ef28a25ee7dc08fd7e78a584ec39698b03385bcac0
7
- data.tar.gz: ce22d223fd1b67df63878d1e1f4d54273437c01a4359f384dc0658ecf06e1e83481285d2b78afd511ee17fabe75a5cb8342826e5cab6ee63cc49a33ccb3c38e4
6
+ metadata.gz: ecd74de131841cb6a00d1fe66ccfa51fe0a0eafe649e0cdf26e90c8602127f0d29d7f55b4cfeda0f13908b0eebe9828d61dc380a7518268e29bda49bd780855d
7
+ data.tar.gz: 9d27ac10782d1e6319e96b517c6da5a819e7b599f15a2e9ae27a5d1f3e33f13d70838bd64390218931ded9be7ec45c3cc28e1e65dc2cedebcbd5faf17fa1ba6a
@@ -1,7 +1,5 @@
1
1
  name: CI/CD
2
2
  on:
3
- push:
4
- branches: [main]
5
3
  pull_request:
6
4
  branches: [main]
7
5
 
@@ -33,31 +31,6 @@ jobs:
33
31
  - run: bundle install && bundle exec rspec
34
32
  - run: bundle exec rubocop
35
33
 
36
- build:
37
- name: Build Image
38
- needs: test
39
- if: github.ref == 'refs/heads/main'
40
- runs-on: ubuntu-latest
41
- permissions:
42
- packages: write
43
- steps:
44
- - uses: actions/checkout@v4
45
- - uses: docker/setup-buildx-action@v3
46
- - uses: docker/login-action@v3
47
- with:
48
- registry: ghcr.io
49
- username: ${{ github.actor }}
50
- password: ${{ secrets.GITHUB_TOKEN }}
51
- - uses: docker/build-push-action@v5
52
- with:
53
- context: .
54
- push: true
55
- tags: |
56
- ghcr.io/legionio/legion:${{ github.sha }}
57
- ghcr.io/legionio/legion:latest
58
- cache-from: type=gha
59
- cache-to: type=gha,mode=max
60
-
61
34
  helm-lint:
62
35
  name: Helm Lint
63
36
  runs-on: ubuntu-latest
@@ -27,3 +27,28 @@ jobs:
27
27
  gh api repos/LegionIO/homebrew-tap/dispatches \
28
28
  -f event_type=build-daemon \
29
29
  -f "client_payload[legionio_version]=${{ needs.release.outputs.version }}"
30
+
31
+ docker-build:
32
+ name: Build Docker Image
33
+ needs: release
34
+ if: needs.release.outputs.changed == 'true'
35
+ runs-on: ubuntu-latest
36
+ permissions:
37
+ packages: write
38
+ steps:
39
+ - uses: actions/checkout@v4
40
+ - uses: docker/setup-buildx-action@v3
41
+ - uses: docker/login-action@v3
42
+ with:
43
+ registry: ghcr.io
44
+ username: ${{ github.actor }}
45
+ password: ${{ secrets.GITHUB_TOKEN }}
46
+ - uses: docker/build-push-action@v5
47
+ with:
48
+ context: .
49
+ push: true
50
+ tags: |
51
+ ghcr.io/legionio/legion:${{ needs.release.outputs.version }}
52
+ ghcr.io/legionio/legion:latest
53
+ cache-from: type=gha
54
+ cache-to: type=gha,mode=max
data/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Legion Changelog
2
2
 
3
+ ## [1.4.85] - 2026-03-20
4
+
5
+ ### Added
6
+ - `legion lex fixes` CLI command to list pending auto-fix patches (filterable by status)
7
+ - `legion lex approve-fix FIX_ID` CLI command to approve LLM-generated fixes
8
+ - `legion lex reject-fix FIX_ID` CLI command to reject LLM-generated fixes
9
+ - `with_data` helper to `legion lex` subcommand class for data-required operations
10
+
11
+ ## [1.4.84] - 2026-03-20
12
+
13
+ ### Added
14
+ - `Legion::Extensions.load_yaml_agents` — loads YAML/JSON agent definitions from `~/.legionio/agents/` or configured directory
15
+ - `generate_yaml_runner` — dynamically generates a runner Module for each agent with `llm`, `script`, and `http` function types
16
+ - YAML agent loading integrated into `hook_extensions` boot sequence
17
+ - Governance API routes under `/api/governance/approvals` (list, show, submit, approve, reject)
18
+ - HTML governance dashboard at `/governance/` with approve/reject buttons, 30s auto-poll, and reviewer dialog
19
+ - Static file serving enabled for `public/` directory in Sinatra
20
+
21
+ ## [1.4.83] - 2026-03-20
22
+
23
+ ### Added
24
+ - `Helpers::Context` for filesystem-based inter-agent context sharing
25
+ - Org chart API endpoint (`GET /api/org-chart`) with dashboard panel
26
+ - Workflow relationship graph API (`GET /api/relationships/graph`)
27
+ - Workflow visualizer web page (`public/workflow/`) with Cytoscape.js
28
+ - `--worktree` flag for `legion chat` with auto-checkpointing
29
+ - `.legion-context/` and `.legion-worktrees/` in generated `.gitignore`
30
+
3
31
  ## [1.4.82] - 2026-03-20
4
32
 
5
33
  ### Added
data/Dockerfile CHANGED
@@ -4,8 +4,9 @@ WORKDIR /app
4
4
  RUN apt-get update && \
5
5
  apt-get install -y --no-install-recommends build-essential libpq-dev git && \
6
6
  rm -rf /var/lib/apt/lists/*
7
- COPY Gemfile Gemfile.lock ./
8
- RUN bundle config set --local deployment true && \
7
+ COPY Gemfile legionio.gemspec ./
8
+ COPY lib/legion/version.rb lib/legion/
9
+ RUN bundle lock && \
9
10
  bundle config set --local without 'development test' && \
10
11
  bundle install --jobs 4 --retry 3
11
12
  COPY . .
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ class API < Sinatra::Base
5
+ module Routes
6
+ module Governance
7
+ def self.registered(app)
8
+ app.helpers GovernanceHelpers
9
+ register_approvals(app)
10
+ end
11
+
12
+ module GovernanceHelpers
13
+ def run_governance_runner(method, **)
14
+ require 'legion/extensions/audit/runners/approval_queue'
15
+ runner = Object.new.extend(Legion::Extensions::Audit::Runners::ApprovalQueue)
16
+ runner.send(method, **)
17
+ rescue LoadError
18
+ halt 503, json_error('service_unavailable', 'lex-audit not available', status_code: 503)
19
+ end
20
+ end
21
+
22
+ def self.register_approvals(app)
23
+ app.get '/api/governance/approvals' do
24
+ require_data!
25
+ result = run_governance_runner(:list_pending,
26
+ tenant_id: params[:tenant_id],
27
+ limit: (params[:limit] || 50).to_i)
28
+ json_response(result)
29
+ end
30
+
31
+ app.get '/api/governance/approvals/:id' do
32
+ require_data!
33
+ result = run_governance_runner(:show_approval, id: params[:id].to_i)
34
+ if result[:success]
35
+ json_response(result)
36
+ else
37
+ halt 404, json_error('not_found', 'Approval not found', status_code: 404)
38
+ end
39
+ end
40
+
41
+ app.post '/api/governance/approvals' do
42
+ require_data!
43
+ body = parse_request_body
44
+ halt 422, json_error('missing_field', 'approval_type is required', status_code: 422) unless body[:approval_type]
45
+ halt 422, json_error('missing_field', 'requester_id is required', status_code: 422) unless body[:requester_id]
46
+
47
+ result = run_governance_runner(:submit,
48
+ approval_type: body[:approval_type],
49
+ payload: body[:payload] || {},
50
+ requester_id: body[:requester_id],
51
+ tenant_id: body[:tenant_id])
52
+ json_response(result, status_code: 201)
53
+ end
54
+
55
+ app.put '/api/governance/approvals/:id/approve' do
56
+ require_data!
57
+ body = parse_request_body
58
+ halt 422, json_error('missing_field', 'reviewer_id is required', status_code: 422) unless body[:reviewer_id]
59
+
60
+ result = run_governance_runner(:approve,
61
+ id: params[:id].to_i,
62
+ reviewer_id: body[:reviewer_id])
63
+ json_response(result)
64
+ end
65
+
66
+ app.put '/api/governance/approvals/:id/reject' do
67
+ require_data!
68
+ body = parse_request_body
69
+ halt 422, json_error('missing_field', 'reviewer_id is required', status_code: 422) unless body[:reviewer_id]
70
+
71
+ result = run_governance_runner(:reject,
72
+ id: params[:id].to_i,
73
+ reviewer_id: body[:reviewer_id])
74
+ json_response(result)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ class API < Sinatra::Base
5
+ module Routes
6
+ module OrgChart
7
+ def self.registered(app)
8
+ app.helpers OrgChartHelpers
9
+ app.get '/api/org-chart' do
10
+ require_data!
11
+ departments = build_org_chart
12
+ json_response({ departments: departments })
13
+ end
14
+ end
15
+
16
+ module OrgChartHelpers
17
+ def build_org_chart
18
+ extensions = Legion::Data::Model::Extension.all
19
+ workers = Legion::Data::Model::DigitalWorker.all
20
+
21
+ extensions.map do |ext|
22
+ functions = Legion::Data::Model::Function.where(extension_id: ext.id).all
23
+ {
24
+ name: ext.name,
25
+ roles: functions.map do |func|
26
+ ext_workers = workers.select { |w| w.extension_name == ext.name }
27
+ {
28
+ name: func.name,
29
+ workers: ext_workers.map { |w| { id: w.id, name: w.name, status: w.lifecycle_state } }
30
+ }
31
+ end
32
+ }
33
+ end
34
+ rescue StandardError
35
+ []
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ class API < Sinatra::Base
5
+ module Routes
6
+ module Workflow
7
+ def self.registered(app)
8
+ app.helpers WorkflowHelpers
9
+ app.get '/api/relationships/graph' do
10
+ require_data!
11
+ graph = build_relationship_graph(
12
+ chain_id: params[:chain_id]&.to_i,
13
+ extension: params[:extension]
14
+ )
15
+ json_response(graph)
16
+ end
17
+ end
18
+
19
+ module WorkflowHelpers
20
+ def build_relationship_graph(chain_id: nil, extension: nil)
21
+ raw = Legion::Graph::Builder.build(chain_id: chain_id)
22
+
23
+ nodes = raw[:nodes].map do |id, meta|
24
+ ext = id.to_s.split('.').first
25
+ { id: id, label: meta[:label], type: meta[:type], extension: ext }
26
+ end
27
+
28
+ edges = raw[:edges].map do |edge|
29
+ { source: edge[:from], target: edge[:to], label: edge[:label] }
30
+ end
31
+
32
+ if extension
33
+ node_ids = nodes.select { |n| n[:extension] == extension }.map { |n| n[:id] }
34
+ nodes = nodes.select { |n| node_ids.include?(n[:id]) }
35
+ edges = edges.select { |e| node_ids.include?(e[:source]) || node_ids.include?(e[:target]) }
36
+ end
37
+
38
+ { nodes: nodes, edges: edges }
39
+ rescue StandardError
40
+ { nodes: [], edges: [] }
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
data/lib/legion/api.rb CHANGED
@@ -34,6 +34,9 @@ require_relative 'api/audit'
34
34
  require_relative 'api/metrics'
35
35
  require_relative 'api/llm'
36
36
  require_relative 'api/catalog'
37
+ require_relative 'api/org_chart'
38
+ require_relative 'api/workflow'
39
+ require_relative 'api/governance'
37
40
 
38
41
  module Legion
39
42
  class API < Sinatra::Base
@@ -42,6 +45,8 @@ module Legion
42
45
 
43
46
  set :show_exceptions, false
44
47
  set :raise_errors, false
48
+ set :public_folder, File.expand_path('../../public', __dir__)
49
+ set :static, true
45
50
 
46
51
  configure do
47
52
  set :logging, nil
@@ -91,6 +96,7 @@ module Legion
91
96
  register Routes::Extensions
92
97
  register Routes::Nodes
93
98
  register Routes::Schedules
99
+ register Routes::Workflow
94
100
  register Routes::Relationships
95
101
  register Routes::Chains
96
102
  register Routes::Settings
@@ -110,6 +116,8 @@ module Legion
110
116
  register Routes::Metrics
111
117
  register Routes::Llm
112
118
  register Routes::ExtensionCatalog
119
+ register Routes::OrgChart
120
+ register Routes::Governance
113
121
 
114
122
  use Legion::Rbac::Middleware if defined?(Legion::Rbac::Middleware)
115
123
 
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module CLI
5
+ class Acp < Thor
6
+ def self.exit_on_failure?
7
+ true
8
+ end
9
+
10
+ desc 'stdio', 'Start ACP agent with stdio transport (default)'
11
+ def stdio
12
+ require 'legion/extensions/acp'
13
+
14
+ transport = Legion::Extensions::Acp::Transport::Stdio.new
15
+ agent = Legion::Extensions::Acp::Runners::Agent.new(transport: transport)
16
+
17
+ transport.log('LegionIO ACP agent started (stdio)')
18
+
19
+ setup_llm if llm_available?
20
+
21
+ transport.run { |msg| agent.dispatch(msg) }
22
+ end
23
+
24
+ default_command :stdio
25
+
26
+ no_commands do
27
+ private
28
+
29
+ def llm_available?
30
+ require 'legion/llm'
31
+ true
32
+ rescue LoadError
33
+ false
34
+ end
35
+
36
+ def setup_llm
37
+ require 'legion/cli/connection'
38
+ Connection.ensure_settings
39
+ Connection.ensure_llm
40
+ rescue StandardError => e
41
+ warn("[lex-acp] LLM setup failed: #{e.message} — running without prompt support")
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -30,6 +30,7 @@ module Legion
30
30
  class_option :fork, type: :string, desc: 'Fork a saved session (load but save as new)'
31
31
  class_option :add_dir, type: :array, default: [], desc: 'Additional directories to include in context'
32
32
  class_option :personality, type: :string, desc: 'Communication style (concise, verbose, educational)'
33
+ class_option :worktree, type: :boolean, default: false, desc: 'Run in isolated git worktree'
33
34
 
34
35
  autoload :Session, 'legion/cli/chat/session'
35
36
  autoload :StatusIndicator, 'legion/cli/chat/status_indicator'
@@ -54,6 +55,7 @@ module Legion
54
55
  load_custom_agents
55
56
 
56
57
  setup_notification_bridge
58
+ setup_worktree(out) if options[:worktree]
57
59
 
58
60
  chat_log.info "session started model=#{@session.model_id} incognito=#{incognito?}"
59
61
  out.banner(version: Legion::VERSION)
@@ -311,6 +313,7 @@ module Legion
311
313
  index: tool_index, total: tool_total
312
314
  })
313
315
  puts out.dim(" [result] #{result_preview}")
316
+ worktree_auto_checkpoint
314
317
  }
315
318
  ) do |chunk|
316
319
  buffer << chunk.content if chunk.content
@@ -409,6 +412,7 @@ module Legion
409
412
  when '/quit', '/exit', '/q'
410
413
  show_session_stats(out)
411
414
  auto_save_session(out)
415
+ cleanup_worktree(out) if @worktree_path
412
416
  raise SystemExit, 0
413
417
  when '/help', '/h'
414
418
  show_help(out)
@@ -774,6 +778,11 @@ module Legion
774
778
  end
775
779
 
776
780
  def handle_rewind(arg, out)
781
+ if @worktree_path
782
+ handle_worktree_rewind(arg, out)
783
+ return
784
+ end
785
+
777
786
  require 'legion/cli/chat/checkpoint'
778
787
  if Chat::Checkpoint.entries.none?
779
788
  out.warn('No checkpoints available to rewind.')
@@ -1149,6 +1158,82 @@ module Legion
1149
1158
  def api_port_for_chat
1150
1159
  4567
1151
1160
  end
1161
+
1162
+ def setup_worktree(out)
1163
+ require 'legion/extensions/exec/helpers/worktree'
1164
+ @worktree_task_id = "chat-#{SecureRandom.hex(4)}"
1165
+ @checkpoint_count = 0
1166
+ wt = Legion::Extensions::Exec::Helpers::Worktree.create(task_id: @worktree_task_id)
1167
+ if wt[:success]
1168
+ @worktree_path = wt[:path]
1169
+ Dir.chdir(@worktree_path)
1170
+ out.success("Worktree created: #{@worktree_path} (branch: #{wt[:branch]})")
1171
+ chat_log.info "worktree created path=#{@worktree_path} branch=#{wt[:branch]}"
1172
+ else
1173
+ out.warn("Worktree creation failed: #{wt[:reason]}. Continuing without worktree.")
1174
+ chat_log.warn "worktree creation failed: #{wt.inspect}"
1175
+ end
1176
+ rescue LoadError
1177
+ out.warn('lex-exec not available. --worktree requires lex-exec. Continuing without worktree.')
1178
+ end
1179
+
1180
+ def worktree_auto_checkpoint
1181
+ return unless @worktree_path
1182
+
1183
+ require 'legion/extensions/exec/helpers/checkpoint'
1184
+ @checkpoint_count += 1
1185
+ Legion::Extensions::Exec::Helpers::Checkpoint.save(
1186
+ worktree_path: @worktree_path,
1187
+ label: "step-#{@checkpoint_count}",
1188
+ task_id: @worktree_task_id
1189
+ )
1190
+ rescue StandardError => e
1191
+ chat_log.debug "worktree checkpoint failed: #{e.message}"
1192
+ end
1193
+
1194
+ def handle_worktree_rewind(arg, out)
1195
+ require 'legion/extensions/exec/helpers/checkpoint'
1196
+ list = Legion::Extensions::Exec::Helpers::Checkpoint.list_checkpoints(task_id: @worktree_task_id)
1197
+ if list[:checkpoints].empty?
1198
+ out.warn('No worktree checkpoints available.')
1199
+ return
1200
+ end
1201
+
1202
+ label = if arg.nil? || arg.strip.empty?
1203
+ list[:checkpoints].last[:label]
1204
+ elsif arg.strip.match?(/\A\d+\z/)
1205
+ target = [list[:checkpoints].size - arg.strip.to_i, 0].max
1206
+ list[:checkpoints][target][:label]
1207
+ else
1208
+ arg.strip
1209
+ end
1210
+
1211
+ result = Legion::Extensions::Exec::Helpers::Checkpoint.restore(
1212
+ worktree_path: @worktree_path, label: label, task_id: @worktree_task_id
1213
+ )
1214
+ if result[:success]
1215
+ chat_log.info "worktree rewind to #{label}"
1216
+ out.success("Restored to checkpoint: #{label}")
1217
+ else
1218
+ out.warn("Rewind failed: #{result[:message]}")
1219
+ end
1220
+ end
1221
+
1222
+ def cleanup_worktree(out)
1223
+ require 'legion/extensions/exec/helpers/worktree'
1224
+ require 'legion/extensions/exec/helpers/checkpoint'
1225
+
1226
+ checkpoints = Legion::Extensions::Exec::Helpers::Checkpoint.list_checkpoints(task_id: @worktree_task_id)
1227
+ if checkpoints[:checkpoints].any?
1228
+ out.info("Worktree has #{checkpoints[:checkpoints].size} checkpoint(s). Branch: legion/#{@worktree_task_id}")
1229
+ out.info('Worktree preserved. Merge manually or run: git worktree remove <path>')
1230
+ else
1231
+ Legion::Extensions::Exec::Helpers::Worktree.remove(task_id: @worktree_task_id)
1232
+ out.info('Worktree removed (no checkpoints).')
1233
+ end
1234
+ rescue StandardError => e
1235
+ chat_log.warn "worktree cleanup error: #{e.message}"
1236
+ end
1152
1237
  end
1153
1238
  end
1154
1239
  end
@@ -17,6 +17,8 @@ module Legion
17
17
  lines << events_section(data[:events] || [])
18
18
  lines << separator
19
19
  lines << health_section(data[:health] || {})
20
+ lines << separator
21
+ lines << org_chart_section(data[:departments] || [])
20
22
  lines << footer_line(data[:fetched_at])
21
23
  lines.flatten.join("\n")
22
24
  end
@@ -63,6 +65,24 @@ module Legion
63
65
  "Health: #{components.empty? ? 'unknown' : components}"
64
66
  end
65
67
 
68
+ def org_chart_section(departments)
69
+ lines = ['Org Chart:']
70
+ if departments.empty?
71
+ lines << ' (no departments)'
72
+ else
73
+ departments.each do |dept|
74
+ lines << " #{dept[:name]}"
75
+ (dept[:roles] || []).each do |role|
76
+ lines << " +-- #{role[:name]}"
77
+ (role[:workers] || []).each do |w|
78
+ lines << " | +-- #{w[:name]} (#{w[:status]})"
79
+ end
80
+ end
81
+ end
82
+ end
83
+ lines
84
+ end
85
+
66
86
  def footer_line(fetched_at)
67
87
  "Last updated: #{fetched_at&.strftime('%H:%M:%S') || 'never'} | Press q to quit, r to refresh"
68
88
  end
@@ -39,11 +39,33 @@ module Legion
39
39
  settings_path = File.join(workspace_dir, 'settings.json')
40
40
  File.write(settings_path, "{}\n") unless File.exist?(settings_path)
41
41
 
42
+ ensure_gitignore_entries(dir)
43
+
42
44
  workspace_dir
43
45
  end
44
46
 
47
+ GITIGNORE_ENTRIES = %w[
48
+ .legion-context/
49
+ .legion-worktrees/
50
+ ].freeze
51
+
45
52
  private
46
53
 
54
+ def ensure_gitignore_entries(dir)
55
+ gitignore_path = File.join(dir, '.gitignore')
56
+ existing = File.exist?(gitignore_path) ? File.read(gitignore_path) : ''
57
+ existing_lines = existing.lines.map(&:chomp)
58
+
59
+ additions = GITIGNORE_ENTRIES.reject { |entry| existing_lines.include?(entry) }
60
+ return if additions.empty?
61
+
62
+ content = existing
63
+ content += "\n" unless content.empty? || content.end_with?("\n")
64
+ content += "# Legion workspace\n" unless existing_lines.any? { |l| l.include?('Legion') }
65
+ content += "#{additions.join("\n")}\n"
66
+ File.write(gitignore_path, content)
67
+ end
68
+
47
69
  def render_template(path, options)
48
70
  template = File.read(path)
49
71
  ERB.new(template, trim_mode: '-').result_with_hash(options: options)
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+
6
+ module Legion
7
+ module CLI
8
+ class LexCliManifest
9
+ attr_reader :cache_dir
10
+
11
+ def initialize(cache_dir: File.expand_path('~/.legionio/cache/cli'))
12
+ @cache_dir = cache_dir
13
+ FileUtils.mkdir_p(@cache_dir)
14
+ end
15
+
16
+ def write_manifest(gem_name:, gem_version:, alias_name:, commands:)
17
+ data = { 'gem' => gem_name, 'version' => gem_version, 'alias' => alias_name,
18
+ 'commands' => serialize_commands(commands) }
19
+ File.write(manifest_path(gem_name), ::JSON.pretty_generate(data))
20
+ end
21
+
22
+ def read_manifest(gem_name)
23
+ path = manifest_path(gem_name)
24
+ return nil unless File.exist?(path)
25
+
26
+ ::JSON.parse(File.read(path))
27
+ end
28
+
29
+ def resolve_alias(name)
30
+ all_manifests.each do |m|
31
+ return m['gem'] if m['alias'] == name
32
+ end
33
+ nil
34
+ end
35
+
36
+ def all_manifests
37
+ Dir.glob(File.join(@cache_dir, 'lex-*.json')).map do |path|
38
+ ::JSON.parse(File.read(path))
39
+ rescue StandardError
40
+ nil
41
+ end.compact
42
+ end
43
+
44
+ def stale?(gem_name, current_version)
45
+ m = read_manifest(gem_name)
46
+ return true unless m
47
+
48
+ m['version'] != current_version
49
+ end
50
+
51
+ private
52
+
53
+ def manifest_path(gem_name)
54
+ File.join(@cache_dir, "#{gem_name}.json")
55
+ end
56
+
57
+ def serialize_commands(commands)
58
+ commands.transform_values do |cmd|
59
+ {
60
+ 'class' => cmd[:class_name],
61
+ 'methods' => cmd[:methods].transform_values { |m| { 'desc' => m[:desc], 'args' => m[:args] } }
62
+ }
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end