Class: Legion::CLI::Setup

Inherits:
Thor
  • Object
show all
Defined in:
lib/legion/cli/setup_command.rb

Constant Summary collapse

LEGION_MCP_ENTRY =
{
  'command' => 'legionio',
  'args'    => %w[mcp stdio]
}.freeze
PACKS =
{
  agentic:   {
    description: 'Cognitive stack: agentic domains, AI providers, and coordination',
    gems:        %w[
      legion-apollo legion-gaia legion-llm legion-mcp legion-rbac
      lex-acp lex-adapter lex-agentic-affect lex-agentic-attention
      lex-agentic-defense lex-agentic-executive lex-agentic-homeostasis
      lex-agentic-imagination lex-agentic-inference lex-agentic-integration
      lex-agentic-language lex-agentic-learning lex-agentic-memory
      lex-agentic-self lex-agentic-social lex-apollo lex-coldstart
      lex-conditioner lex-detect lex-extinction lex-kerberos lex-knowledge
      lex-llm lex-llm-anthropic lex-llm-azure-foundry lex-llm-bedrock
      lex-llm-gemini lex-llm-mlx lex-llm-ollama lex-llm-openai
      lex-llm-vertex lex-llm-vllm lex-metering lex-mesh
      lex-microsoft_teams lex-mind-growth lex-node lex-privatecore
      lex-synapse lex-telemetry lex-tick
    ]
  },
  llm:       {
    description: 'LLM routing and provider integration (no cognitive stack)',
    gems:        %w[
      legion-llm legion-mcp lex-llm lex-llm-anthropic lex-llm-azure-foundry
      lex-llm-bedrock lex-llm-gemini lex-llm-mlx
      lex-llm-ollama lex-llm-openai lex-llm-vertex lex-llm-vllm
    ]
  },
  gaia:      {
    description: 'Cognitive coordination engine + agentic extensions (GAIA stack)',
    gems:        %w[
      legion-gaia
      lex-agentic-affect lex-agentic-attention lex-agentic-defense
      lex-agentic-executive lex-agentic-homeostasis lex-agentic-inference
      lex-agentic-integration lex-agentic-language lex-agentic-learning
      lex-agentic-memory lex-agentic-self lex-agentic-social
      lex-synapse lex-mind-growth lex-tick
    ]
  },
  identity:  {
    description: 'Identity and access management (RBAC + identity providers)',
    gems:        %w[
      legion-rbac lex-identity-entra lex-identity-kerberos lex-identity-system lex-kerberos
    ]
  },
  developer: {
    description: 'Developer tooling and CI/CD integrations',
    gems:        %w[
      lex-developer lex-dynatrace lex-eval lex-exec lex-github
      lex-http lex-jfrog lex-skill-superpowers lex-ssh
    ]
  },
  channels:  {
    description: 'Channel adapters for chat platforms',
    gems:        %w[lex-slack lex-microsoft_teams]
  }
}.freeze
PYTHON_PACKAGES =
Legion::Python::PACKAGES
PYTHON_VENV_DIR =
Legion::Python::VENV_DIR
PYTHON_MARKER =
Legion::Python::MARKER
SKILL_CONTENT =
<<~MARKDOWN
  ---
  name: legion
  description: Orchestrate LegionIO extensions and agents
  ---

  You have access to LegionIO MCP tools. When the user asks you to work with Legion:

  1. Use `legion.discover_tools` to find relevant capabilities
  2. Use `legion.do_action` for natural language task routing
  3. Use `legion.run_task` to execute specific extension functions
  4. Use `legion.list_peers` and `legion.ask_peer` for agent coordination
  5. Present results as a consolidated summary
MARKDOWN

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.exit_on_failure?Boolean

Returns:

  • (Boolean)


17
18
19
# File 'lib/legion/cli/setup_command.rb', line 17

def self.exit_on_failure?
  true
end

Instance Method Details

#agenticObject



190
191
192
# File 'lib/legion/cli/setup_command.rb', line 190

def agentic
  install_pack(:agentic)
end

#channelsObject



216
217
218
# File 'lib/legion/cli/setup_command.rb', line 216

def channels
  install_pack(:channels)
end

#claude_codeObject



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/legion/cli/setup_command.rb', line 106

def claude_code
  out = formatter
  installed = []

  install_claude_mcp(installed)
  install_claude_skill(installed)
  install_claude_hooks(installed)

  if options[:json]
    out.json(platform: 'claude-code', installed: installed)
  else
    out.spacer
    out.success("Legion configured for Claude Code (#{installed.size} item(s))")
    out.spacer
    puts "  Run '/legion' in Claude Code to use your LegionIO tools."
  end
end

#cursorObject



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/legion/cli/setup_command.rb', line 125

def cursor
  out = formatter
  path = File.join(Dir.pwd, '.cursor', 'mcp.json')
  installed = []

  write_mcp_servers_json(nil, path, installed)

  if options[:json]
    out.json(platform: 'cursor', installed: installed)
  else
    out.spacer
    out.success("Legion configured for Cursor (#{installed.size} item(s))")
    out.spacer
    puts "  MCP config written to: #{path}"
  end
end

#fleetObject



223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/legion/cli/setup_command.rb', line 223

def fleet
  require 'legion/cli/fleet_setup'
  setup = Legion::CLI::FleetSetup.new(formatter: formatter, options: options)

  if options[:dry_run]
    gems = Legion::CLI::FleetSetup.fleet_gems
    installed, missing = gems.partition { |g| Gem::Specification.find_by_name(g) rescue nil } # rubocop:disable Style/RescueModifier
    if options[:json]
      formatter.json(to_install: missing, already_installed: installed)
    else
      formatter.header('Fleet Setup (dry run)')
      missing.each { |g| puts "  install  #{g}" }
      installed.each { |g| puts "  skip     #{g} (already installed)" }
    end
    return
  end

  case options[:phase]
  when 1
    result = setup.phase1_install
  when 2
    Connection.ensure_data
    result = setup.phase2_wire
    Connection.shutdown
  else
    result = setup.phase1_install
    if result[:success]
      formatter.spacer unless options[:json]
      formatter.warn('Phase 2 requires LegionIO restart to register extensions.') unless options[:json]
      formatter.warn('Run: legionio start && legionio setup fleet --phase 2') unless options[:json]
    end
  end

  formatter.json(result) if options[:json]
rescue SystemExit
  raise
rescue StandardError => e
  formatter.error("Fleet setup failed: #{e.message}")
  raise SystemExit, 1
end

#gaiaObject



204
205
206
# File 'lib/legion/cli/setup_command.rb', line 204

def gaia
  install_pack(:gaia)
end

#identityObject



210
211
212
# File 'lib/legion/cli/setup_command.rb', line 210

def identity
  install_pack(:identity)
end

#llmObject



198
199
200
# File 'lib/legion/cli/setup_command.rb', line 198

def llm
  install_pack(:llm)
end

#packsObject



333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/legion/cli/setup_command.rb', line 333

def packs
  out = formatter
  pack_statuses = PACKS.map do |name, pack|
    installed, missing = partition_gems(pack[:gems])
    { name: name, description: pack[:description],
      installed: installed.map { |g| { name: g, version: gem_version(g) } },
      missing: missing }
  end

  if options[:json]
    out.json(packs: pack_statuses)
  else
    out.header('Feature Packs')
    out.spacer
    pack_statuses.each do |ps|
      all_installed = ps[:missing].empty?
      icon = all_installed ? out.colorize('installed', :success) : out.colorize('not installed', :muted)
      puts "  #{out.colorize(ps[:name].to_s.ljust(12), :label)} #{icon}  #{ps[:description]}"
      ps[:installed].each do |g|
        puts "    #{out.colorize(g[:name], :success)} #{g[:version]}"
      end
      ps[:missing].each do |g|
        puts "    #{out.colorize(g, :muted)} (missing)"
      end
    end
    out.spacer
  end
end

#proxy_modeObject



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/legion/cli/setup_command.rb', line 163

def proxy_mode
  out = formatter
  base_url = "http://#{options[:host]}:#{options[:port]}/v1"
  written = []
  skipped = []

  write_codex_config(base_url, written, skipped)
  write_claude_code_proxy_config(base_url, written, skipped)

  if options[:json]
    out.json(written: written, skipped: skipped, base_url: base_url)
  else
    out.spacer
    out.success("LegionIO proxy mode configured (#{written.size} written, #{skipped.size} skipped)")
    written.each { |f| puts "  Written:  #{f}" }
    skipped.each { |f| puts "  Skipped (already exists, use --force to overwrite): #{f}" }
    out.spacer
    puts "  LegionIO API: #{base_url.sub('/v1', '')}"
    puts '  Codex CLI:    legion llm proxy (uses ~/.codex/config.toml)'
    puts '  Claude Code:  set ANTHROPIC_BASE_URL in your shell or ~/.claude/settings.json'
    out.spacer
  end
end

#pythonObject

rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/legion/cli/setup_command.rb', line 267

def python # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
  out = formatter
  results = []

  python3 = find_python3
  unless python3
    out.error('python3 not found. Install it with: brew install python')
    exit 1
  end

  if options[:rebuild] && Dir.exist?(PYTHON_VENV_DIR)
    out.header("Rebuilding Python venv at #{PYTHON_VENV_DIR}") unless options[:json]
    FileUtils.rm_rf(PYTHON_VENV_DIR)
  end

  unless File.exist?("#{PYTHON_VENV_DIR}/pyvenv.cfg")
    out.header("Creating Python venv at #{PYTHON_VENV_DIR}") unless options[:json]
    FileUtils.mkdir_p(File.dirname(PYTHON_VENV_DIR))
    unless system(python3, '-m', 'venv', PYTHON_VENV_DIR)
      out.error('Failed to create Python venv')
      exit 1
    end
    results << { action: 'created_venv', path: PYTHON_VENV_DIR }
  end

  pip = "#{PYTHON_VENV_DIR}/bin/pip"
  unless File.executable?(pip)
    out.error("pip not found at #{pip} — try: legionio setup python --rebuild")
    exit 1
  end

  packages = PYTHON_PACKAGES + Array(options[:packages])
  packages.uniq!

  failed = false
  packages.each do |pkg|
    puts "  Installing #{pkg}..." unless options[:json]
    output, status = Open3.capture2e(pip, 'install', '--quiet', '--upgrade', pkg)
    if status.success?
      out.success("  #{pkg}") unless options[:json]
      results << { package: pkg, status: 'installed' }
    else
      failed = true
      out.error("  #{pkg} failed") unless options[:json]
      results << { package: pkg, status: 'failed', error: output.strip.lines.last&.strip }
    end
  end

  write_python_marker(python3, packages)

  if options[:json]
    out.json(venv: PYTHON_VENV_DIR, python: python_version(python3), results: results)
  else
    out.spacer
    out.success("Python environment ready: #{PYTHON_VENV_DIR}/bin/python3")
    out.spacer
    puts "  Interpreter:    #{PYTHON_VENV_DIR}/bin/python3"
    puts '  Env var:        $LEGION_PYTHON'
    puts '  Add packages:   legionio setup python --packages <name> [<name>...]'
    puts '  Rebuild venv:   legionio setup python --rebuild'
  end

  exit 1 if failed
end

#statusObject



363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
# File 'lib/legion/cli/setup_command.rb', line 363

def status
  out = formatter
  platforms = check_all_platforms

  if options[:json]
    out.json(platforms: platforms)
  else
    out.header('Legion MCP Setup Status')
    out.spacer
    platforms.each do |p|
      icon = p[:configured] ? out.colorize('configured', :success) : out.colorize('not configured', :muted)
      puts "  #{out.colorize(p[:name].ljust(16), :label)} #{icon}"
      puts "    #{out.colorize(p[:path], :muted)}" if p[:path]
    end
    out.spacer
    configured_count = platforms.count { |p| p[:configured] }
    puts "  #{configured_count} of #{platforms.size} platform(s) configured"
  end
end

#vscodeObject



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/legion/cli/setup_command.rb', line 143

def vscode
  out = formatter
  path = File.join(Dir.pwd, '.vscode', 'mcp.json')
  installed = []

  write_vscode_mcp_json(nil, path, installed)

  if options[:json]
    out.json(platform: 'vscode', installed: installed)
  else
    out.spacer
    out.success("Legion configured for VS Code (#{installed.size} item(s))")
    out.spacer
    puts "  MCP config written to: #{path}"
  end
end