legion-tty 0.2.0 → 0.2.5

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: ea1c7546376e19d51fa1dde5e9f01184b74d41889c9f131f511aa90f634652d9
4
- data.tar.gz: d1c354bb8dcbb056429d4e5eeaafbe7acc7fedaf15cc990ff96ec5d1f2561f6b
3
+ metadata.gz: 56a5f810aa64c98df76de0ed9f8b6637d2e18773f684158f6320f610f0b3f212
4
+ data.tar.gz: cf98c2b4cd4c37ee03e0e59067182f6237f9144d482ba2259a55b825bd4ef840
5
5
  SHA512:
6
- metadata.gz: 45ddd593ac68b8bdac3b28497f3f60120ed09078b4d3a1d6a870b5a5ffa0797fb2c7debfbe8ee48d0edf87b30a64b43c60195c710adddfdc7eb2b6c55d3d7b85
7
- data.tar.gz: da55989963a0b796421c8050d8d434e7be6a331a7a6cd379f0d16a77a3859e21a49ca37a82468dabdf1f03c78936fb8609526d549f7b52472b4c44cc86f8fa13
6
+ metadata.gz: e914e40a1b9ed5e0672c5e366cbe052bd7f372e8786e23b4455c998c99a4ef27f58fb3655ddfe3f8ef8d788ffc07b32671f03f6d6b754354c830ed9ef6fb076c
7
+ data.tar.gz: 4b94d15261c0d5bc1e3ecf783fb9235167df51fb9154450087c7cef6e6ec3e45a1a396db683b1c79640c9703069a5c725b64433162e36603a3afd60d0070322f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.5] - 2026-03-18
4
+
5
+ ### Added
6
+ - Bootstrap config auto-import: detects `LEGIONIO_BOOTSTRAP_CONFIG` env var during onboarding
7
+ - Fetches config from local file or HTTP/HTTPS URL (raw JSON or base64-encoded)
8
+ - Splits top-level keys into individual settings files (`~/.legionio/settings/{key}.json`)
9
+ - Deep merges with existing config files if present
10
+ - Bootstrap summary line in reveal box showing imported sections
11
+ - 14 new specs (374 total)
12
+
13
+ ## [0.2.4] - 2026-03-18
14
+
15
+ ### Added
16
+ - Cache service awakening: detects running Redis/Memcached, offers to activate if missing
17
+ - GAIA daemon discovery: checks for running legionio daemon, offers to start it
18
+ - Cinematic typed prompts: "extending neural pathways", "GAIA is awake", "cognitive threads synchronized"
19
+ - Cache and GAIA status lines in reveal summary box
20
+ - 46 new specs (360 total)
21
+
22
+ ## [0.2.3] - 2026-03-18
23
+
24
+ ### Added
25
+ - Vault LDAP auth step in onboarding: prompts to connect to configured vault clusters
26
+ - Default username from Kerberos samaccountname or `$USER`, hidden password input
27
+ - `WizardPrompt#ask_secret` (masked) and `#ask_with_default` methods
28
+ - Vault cluster connection status in reveal summary box
29
+ - 26 new specs for vault auth and wizard prompt methods
30
+
31
+ ## [0.2.2] - 2026-03-18
32
+
33
+ ### Added
34
+ - Dotfile scanning: gitconfig (name, email, signing key), JFrog CLI servers, Terraform credential hosts
35
+ - `Scanner#scan_dotfiles`, `#scan_gitconfig`, `#scan_jfrog`, `#scan_terraform` methods
36
+ - Onboarding reveal box now displays discovered dotfile configuration
37
+ - Specs for all dotfile scanning and summary display methods
38
+
39
+ ## [0.2.1] - 2026-03-18
40
+
41
+ ### Changed
42
+ - Onboarding replaces credential prompts with LLM provider auto-detection and ping-testing
43
+ - Shows green checkmark or red X with latency for each configured provider
44
+ - Auto-selects default provider or lets user choose if multiple are available
45
+
46
+ ### Added
47
+ - `Background::LlmProbe` for async provider ping-testing during onboarding
48
+ - `WizardPrompt#display_provider_results` and `#select_default_provider` methods
49
+ - Bootsnap and YJIT startup optimizations in `exe/legion-tty`
50
+
3
51
  ## [0.2.0] - 2026-03-18
4
52
 
5
53
  ### Added
data/exe/legion-tty CHANGED
@@ -5,9 +5,9 @@ RubyVM::YJIT.enable if defined?(RubyVM::YJIT)
5
5
 
6
6
  require 'bootsnap'
7
7
  Bootsnap.setup(
8
- cache_dir: File.expand_path('~/.legionio/cache/bootsnap'),
9
- development_mode: false,
10
- load_path_cache: true,
8
+ cache_dir: File.expand_path('~/.legionio/cache/bootsnap'),
9
+ development_mode: false,
10
+ load_path_cache: true,
11
11
  compile_cache_iseq: true,
12
12
  compile_cache_yaml: true
13
13
  )
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'fileutils'
5
+ require 'json'
6
+ require 'net/http'
7
+ require 'uri'
8
+
9
+ module Legion
10
+ module TTY
11
+ module Background
12
+ class BootstrapConfig
13
+ ENV_KEY = 'LEGIONIO_BOOTSTRAP_CONFIG'
14
+ SETTINGS_DIR = File.expand_path('~/.legionio/settings')
15
+
16
+ def initialize(logger: nil)
17
+ @log = logger
18
+ end
19
+
20
+ def run_async(queue)
21
+ Thread.new do
22
+ result = perform_bootstrap
23
+ queue.push(result)
24
+ rescue StandardError => e
25
+ @log&.log('bootstrap', "ERROR: #{e.class}: #{e.message}")
26
+ queue.push({ type: :bootstrap_error, error: e.message })
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def perform_bootstrap
33
+ @log&.log('bootstrap', 'checking for bootstrap config')
34
+ source = ENV.fetch(ENV_KEY, nil)
35
+ return skip_result unless source && !source.empty?
36
+
37
+ @log&.log('bootstrap', "source: #{source}")
38
+ body = fetch_source(source)
39
+ config = parse_payload(body)
40
+ written = write_split_config(config)
41
+ @log&.log('bootstrap', "wrote #{written.size} config files: #{written.join(', ')}")
42
+ { type: :bootstrap_complete, data: { files: written, sections: config.keys.map(&:to_s) } }
43
+ end
44
+
45
+ def skip_result
46
+ @log&.log('bootstrap', "#{ENV_KEY} not set, skipping")
47
+ { type: :bootstrap_complete, data: nil }
48
+ end
49
+
50
+ def fetch_source(source)
51
+ if source.match?(%r{\Ahttps?://}i)
52
+ fetch_http(source)
53
+ else
54
+ path = File.expand_path(source)
55
+ raise "File not found: #{source}" unless File.exist?(path)
56
+
57
+ File.read(path)
58
+ end
59
+ end
60
+
61
+ def fetch_http(url)
62
+ uri = URI.parse(url)
63
+ http = Net::HTTP.new(uri.host, uri.port)
64
+ http.use_ssl = uri.scheme == 'https'
65
+ http.open_timeout = 10
66
+ http.read_timeout = 10
67
+ request = Net::HTTP::Get.new(uri)
68
+ response = http.request(request)
69
+ raise "HTTP #{response.code}: #{response.message}" unless response.is_a?(Net::HTTPSuccess)
70
+
71
+ response.body
72
+ end
73
+
74
+ def parse_payload(body)
75
+ parsed = ::JSON.parse(body, symbolize_names: true)
76
+ raise 'Config must be a JSON object' unless parsed.is_a?(Hash)
77
+
78
+ parsed
79
+ rescue ::JSON::ParserError
80
+ decoded = Base64.decode64(body)
81
+ parsed = ::JSON.parse(decoded, symbolize_names: true)
82
+ raise 'Config must be a JSON object' unless parsed.is_a?(Hash)
83
+
84
+ parsed
85
+ end
86
+
87
+ def write_split_config(config)
88
+ FileUtils.mkdir_p(SETTINGS_DIR)
89
+ written = []
90
+
91
+ config.each do |key, value|
92
+ next unless value.is_a?(Hash)
93
+
94
+ path = File.join(SETTINGS_DIR, "#{key}.json")
95
+ content = { key => value }
96
+
97
+ if File.exist?(path)
98
+ existing = ::JSON.parse(File.read(path), symbolize_names: true)
99
+ content = deep_merge(existing, content)
100
+ end
101
+
102
+ File.write(path, ::JSON.pretty_generate(content))
103
+ written << "#{key}.json"
104
+ end
105
+
106
+ written
107
+ end
108
+
109
+ def deep_merge(base, overlay)
110
+ base.merge(overlay) do |_key, old_val, new_val|
111
+ if old_val.is_a?(Hash) && new_val.is_a?(Hash)
112
+ deep_merge(old_val, new_val)
113
+ else
114
+ new_val
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module TTY
5
+ module Background
6
+ class LlmProbe
7
+ def initialize(logger: nil)
8
+ @log = logger
9
+ end
10
+
11
+ def run_async(queue)
12
+ Thread.new do
13
+ result = probe_providers
14
+ queue.push({ data: result })
15
+ rescue StandardError => e
16
+ @log&.log('llm_probe', "error: #{e.message}")
17
+ queue.push({ data: { providers: [], error: e.message } })
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def probe_providers
24
+ require 'legion/llm'
25
+ require 'legion/settings'
26
+ start_llm
27
+ results = collect_provider_results
28
+ { providers: results }
29
+ end
30
+
31
+ def start_llm
32
+ Legion::LLM.start unless Legion::LLM.started?
33
+ rescue StandardError => e
34
+ @log&.log('llm_probe', "LLM start failed: #{e.message}")
35
+ end
36
+
37
+ def collect_provider_results
38
+ providers = Legion::LLM.settings[:providers] || {}
39
+ providers.filter_map do |name, config|
40
+ next unless config[:enabled]
41
+
42
+ result = ping_provider(name, config)
43
+ @log&.log('llm_probe', "#{name}: #{result[:status]} (#{result[:latency_ms]}ms)")
44
+ result
45
+ end
46
+ end
47
+
48
+ def ping_provider(name, config)
49
+ model = config[:default_model]
50
+ start_time = Time.now
51
+ RubyLLM.chat(model: model, provider: name).ask('Respond with only: pong')
52
+ latency = ((Time.now - start_time) * 1000).round
53
+ { name: name, model: model, status: :ok, latency_ms: latency }
54
+ rescue StandardError => e
55
+ latency = ((Time.now - start_time) * 1000).round
56
+ { name: name, model: model, status: :error, latency_ms: latency, error: e.message }
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -60,7 +60,15 @@ module Legion
60
60
 
61
61
  def scan_all
62
62
  { services: scan_services, repos: scan_git_repos, tools: scan_shell_history,
63
- configs: scan_config_files }
63
+ configs: scan_config_files, dotfiles: scan_dotfiles }
64
+ end
65
+
66
+ def scan_dotfiles
67
+ {
68
+ git: scan_gitconfig,
69
+ jfrog: scan_jfrog,
70
+ terraform: scan_terraform
71
+ }
64
72
  end
65
73
 
66
74
  def run_async(queue)
@@ -153,6 +161,49 @@ module Legion
153
161
  def extract_command(line)
154
162
  line.sub(/^: \d+:\d+;/, '').split.first
155
163
  end
164
+
165
+ def scan_gitconfig
166
+ name = `git config --global user.name 2>/dev/null`.strip
167
+ email = `git config --global user.email 2>/dev/null`.strip
168
+ signing_key = `git config --global user.signingkey 2>/dev/null`.strip
169
+ return nil if name.empty? && email.empty?
170
+
171
+ result = { name: name.empty? ? nil : name, email: email.empty? ? nil : email }
172
+ result[:signing_key] = signing_key unless signing_key.empty?
173
+ result
174
+ rescue StandardError
175
+ nil
176
+ end
177
+
178
+ def scan_jfrog
179
+ config_path = File.expand_path('~/.jfrog/jfrog-cli.conf.v6')
180
+ return nil unless File.exist?(config_path)
181
+
182
+ require 'json'
183
+ data = ::JSON.parse(File.read(config_path), symbolize_names: true)
184
+ servers = data[:servers]
185
+ return nil unless servers.is_a?(Array) && !servers.empty?
186
+
187
+ servers.map do |s|
188
+ { server_id: s[:serverId], url: s[:url], user: s[:user] }
189
+ end
190
+ rescue StandardError
191
+ nil
192
+ end
193
+
194
+ def scan_terraform
195
+ creds_path = File.expand_path('~/.terraform.d/credentials.tfrc.json')
196
+ return nil unless File.exist?(creds_path)
197
+
198
+ require 'json'
199
+ data = ::JSON.parse(File.read(creds_path), symbolize_names: true)
200
+ hosts = data[:credentials]&.keys || []
201
+ return nil if hosts.empty?
202
+
203
+ { hosts: hosts.map(&:to_s) }
204
+ rescue StandardError
205
+ nil
206
+ end
156
207
  end
157
208
  # rubocop:enable Metrics/ClassLength
158
209
  end
@@ -34,6 +34,14 @@ module Legion
34
34
  @prompt.mask("Enter API key for #{provider}:")
35
35
  end
36
36
 
37
+ def ask_secret(question)
38
+ @prompt.mask(question)
39
+ end
40
+
41
+ def ask_with_default(question, default)
42
+ @prompt.ask(question, default: default)
43
+ end
44
+
37
45
  # rubocop:disable Naming/PredicateMethod
38
46
  def confirm(question)
39
47
  @prompt.yes?(question)
@@ -43,6 +51,26 @@ module Legion
43
51
  def select_from(question, choices)
44
52
  @prompt.select(question, choices)
45
53
  end
54
+
55
+ def display_provider_results(providers)
56
+ providers.each do |p|
57
+ icon = p[:status] == :ok ? "\u2705" : "\u274C"
58
+ latency = "#{p[:latency_ms]}ms"
59
+ label = "#{icon} #{p[:name]} (#{p[:model]}) \u2014 #{latency}"
60
+ label += " [#{p[:error]}]" if p[:error]
61
+ @prompt.say(label)
62
+ end
63
+ end
64
+
65
+ def select_default_provider(working_providers)
66
+ return nil if working_providers.empty?
67
+ return working_providers.first[:name] if working_providers.size == 1
68
+
69
+ choices = working_providers.map do |p|
70
+ { name: "#{p[:name]} (#{p[:model]}, #{p[:latency_ms]}ms)", value: p[:name] }
71
+ end
72
+ @prompt.select('Multiple providers available. Choose your default:', choices)
73
+ end
46
74
  end
47
75
  end
48
76
  end
@@ -6,6 +6,7 @@ require_relative '../components/wizard_prompt'
6
6
  require_relative '../background/scanner'
7
7
  require_relative '../background/github_probe'
8
8
  require_relative '../background/kerberos_probe'
9
+ require_relative '../background/bootstrap_config'
9
10
  require_relative '../boot_logger'
10
11
  require_relative '../theme'
11
12
 
@@ -25,11 +26,16 @@ module Legion
25
26
  @github_queue = Queue.new
26
27
  @github_quick_queue = Queue.new
27
28
  @kerberos_queue = Queue.new
29
+ @llm_queue = Queue.new
30
+ @bootstrap_queue = Queue.new
28
31
  @kerberos_identity = nil
29
32
  @github_quick = nil
33
+ @vault_results = nil
34
+ @bootstrap_data = nil
30
35
  @log = BootLogger.new
31
36
  end
32
37
 
38
+ # rubocop:disable Metrics/AbcSize
33
39
  def activate
34
40
  @log.log('onboarding', 'activate started')
35
41
  start_background_threads
@@ -37,11 +43,16 @@ module Legion
37
43
  run_intro
38
44
  config = run_wizard
39
45
  @log.log('wizard', "name=#{config[:name]} provider=#{config[:provider]}")
46
+ collect_bootstrap_result
47
+ run_vault_auth
40
48
  scan_data, github_data = collect_background_results
49
+ run_cache_awakening(scan_data)
50
+ run_gaia_awakening
41
51
  run_reveal(name: config[:name], scan_data: scan_data, github_data: github_data)
42
52
  @log.log('onboarding', 'activate complete')
43
53
  build_onboarding_result(config, scan_data, github_data)
44
54
  end
55
+ # rubocop:enable Metrics/AbcSize
45
56
 
46
57
  # rubocop:disable Metrics/AbcSize
47
58
  def run_rain
@@ -92,13 +103,34 @@ module Legion
92
103
  typed_output(" Nice to meet you, #{name}.")
93
104
  @output.puts
94
105
  sleep 1
95
- typed_output("Let's get you connected.")
106
+ providers = detect_providers
107
+ default = select_provider_default(providers)
108
+ @output.puts
109
+ { name: name, provider: default, providers: providers }
110
+ end
111
+
112
+ def detect_providers
113
+ typed_output('Detecting AI providers...')
96
114
  @output.puts
97
115
  @output.puts
98
- provider = @wizard.select_provider
99
- sleep 0.5
100
- api_key = @wizard.ask_api_key(provider: provider)
101
- { name: name, provider: provider, api_key: api_key }
116
+ llm_data = drain_with_timeout(@llm_queue, timeout: 15)
117
+ providers = llm_data&.dig(:data, :providers) || []
118
+ @wizard.display_provider_results(providers)
119
+ @output.puts
120
+ providers
121
+ end
122
+
123
+ def select_provider_default(providers)
124
+ working = providers.select { |p| p[:status] == :ok }
125
+ if working.any?
126
+ default = @wizard.select_default_provider(working)
127
+ sleep 0.5
128
+ typed_output("Connected. Let's chat.")
129
+ default
130
+ else
131
+ typed_output('No AI providers detected. Configure one in ~/.legionio/settings/llm.json')
132
+ nil
133
+ end
102
134
  end
103
135
 
104
136
  def start_background_threads
@@ -109,7 +141,81 @@ module Legion
109
141
  @scanner.run_async(@scan_queue)
110
142
  @kerberos_probe.run_async(@kerberos_queue)
111
143
  @github_probe.run_quick_async(@github_quick_queue)
144
+ require_relative '../background/llm_probe'
145
+ @llm_probe = Background::LlmProbe.new(logger: @log)
146
+ @llm_probe.run_async(@llm_queue)
147
+ @bootstrap_probe = Background::BootstrapConfig.new(logger: @log)
148
+ @bootstrap_probe.run_async(@bootstrap_queue)
149
+ end
150
+
151
+ def run_cache_awakening(scan_data)
152
+ services = scan_data.is_a?(Hash) ? scan_data[:services] : nil
153
+ return unless services.is_a?(Hash)
154
+
155
+ if services.dig(:memcached, :running) || services.dig(:redis, :running)
156
+ typed_output('... extending neural pathways...')
157
+ sleep 0.8
158
+ typed_output('Additional memory online.')
159
+ else
160
+ run_cache_offer
161
+ end
162
+ @output.puts
163
+ end
164
+
165
+ def run_cache_offer
166
+ typed_output('No extended memory detected.')
167
+ sleep 0.8
168
+ @output.puts
169
+ return unless @wizard.confirm('Shall I activate a memory cache?')
170
+
171
+ binary = detect_cache_binary
172
+ if binary
173
+ run_cache_start(binary)
174
+ else
175
+ typed_output('No cache service found. Install with: brew install memcached')
176
+ end
177
+ end
178
+
179
+ def run_cache_start(binary)
180
+ started = start_cache_service(binary.to_s)
181
+ if started
182
+ typed_output('Memory cache activated. Neural capacity expanded.')
183
+ else
184
+ typed_output("No cache service found. Install with: brew install #{binary}")
185
+ end
186
+ end
187
+
188
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
189
+ def run_gaia_awakening
190
+ typed_output('Scanning for active cognition threads...')
191
+ sleep 1.2
192
+ @output.puts
193
+
194
+ if legionio_running?
195
+ typed_output('GAIA is awake.')
196
+ sleep 0.5
197
+ typed_output('Heuristic mesh: nominal.')
198
+ sleep 0.8
199
+ typed_output('Cognitive threads synchronized.')
200
+ else
201
+ typed_output('GAIA is dormant.')
202
+ sleep 1
203
+ @output.puts
204
+ if @wizard.confirm('Shall I wake her?')
205
+ started = start_legionio_daemon
206
+ if started
207
+ typed_output('... initializing cognitive substrate...')
208
+ sleep 1
209
+ typed_output('GAIA online. All systems nominal.')
210
+ else
211
+ typed_output("Could not start daemon. Run 'legionio start' manually.")
212
+ end
213
+ end
214
+ end
215
+
216
+ @output.puts
112
217
  end
218
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
113
219
 
114
220
  def collect_background_results
115
221
  @log.log('collect', 'waiting for scanner results (10s timeout)')
@@ -145,16 +251,104 @@ module Legion
145
251
  sleep 1
146
252
  end
147
253
 
254
+ # rubocop:disable Metrics/AbcSize
148
255
  def build_summary(name:, scan_data:, github_data:)
149
256
  lines = ["Hello, #{name}!", '', "Here's what I found:"]
257
+ lines.concat(bootstrap_summary_lines)
150
258
  lines.concat(identity_summary_lines)
151
259
  lines.concat(scan_summary_lines(scan_data))
260
+ lines.concat(dotfiles_summary_lines(scan_data))
152
261
  lines.concat(github_summary_lines(github_data))
262
+ lines.concat(vault_summary_lines)
263
+ lines.concat(cache_summary_lines(scan_data))
264
+ lines.concat(gaia_summary_lines)
153
265
  lines.join("\n")
154
266
  end
267
+ # rubocop:enable Metrics/AbcSize
155
268
 
156
269
  private
157
270
 
271
+ def collect_bootstrap_result
272
+ result = drain_with_timeout(@bootstrap_queue, timeout: 5)
273
+ @bootstrap_data = result&.dig(:data)
274
+ return unless @bootstrap_data
275
+
276
+ files = @bootstrap_data[:files] || []
277
+ @log.log('bootstrap', "imported #{files.size} config files: #{files.join(', ')}")
278
+ typed_output('Configuration loaded.')
279
+ @output.puts
280
+ end
281
+
282
+ def run_vault_auth
283
+ return unless vault_clusters_configured?
284
+
285
+ count = vault_cluster_count
286
+ @output.puts
287
+ typed_output("I found #{count} Vault cluster#{'s' if count != 1}.")
288
+ @output.puts
289
+ return unless @wizard.confirm('Connect now?')
290
+
291
+ run_vault_auth_credentials
292
+ end
293
+
294
+ def run_vault_auth_credentials
295
+ username = @wizard.ask_with_default('Username:', default_vault_username)
296
+ password = @wizard.ask_secret('Password:')
297
+ return if password.nil? || password.empty?
298
+
299
+ @output.puts
300
+ typed_output('Authenticating...')
301
+ @output.puts
302
+
303
+ @vault_results = perform_vault_auth(username, password)
304
+ display_vault_results(@vault_results)
305
+ end
306
+
307
+ def vault_clusters_configured?
308
+ return false unless defined?(Legion::Settings)
309
+
310
+ clusters = Legion::Settings.dig(:crypt, :vault, :clusters)
311
+ clusters.is_a?(Hash) && clusters.any?
312
+ rescue StandardError
313
+ false
314
+ end
315
+
316
+ def vault_cluster_count
317
+ Legion::Settings.dig(:crypt, :vault, :clusters)&.size || 0
318
+ rescue StandardError
319
+ 0
320
+ end
321
+
322
+ def default_vault_username
323
+ if @kerberos_identity&.dig(:samaccountname)
324
+ @kerberos_identity[:samaccountname]
325
+ else
326
+ ENV.fetch('USER', 'unknown')
327
+ end
328
+ end
329
+
330
+ def perform_vault_auth(username, password)
331
+ return {} unless defined?(Legion::Crypt) && Legion::Crypt.respond_to?(:ldap_login_all)
332
+
333
+ Legion::Crypt.ldap_login_all(username: username, password: password)
334
+ rescue StandardError => e
335
+ @log.log('vault', "LDAP auth failed: #{e.message}")
336
+ {}
337
+ end
338
+
339
+ def display_vault_results(results)
340
+ results.each do |name, result|
341
+ if result[:error]
342
+ @output.puts " #{Theme.c(:error, 'X')} #{name}: #{result[:error]}"
343
+ else
344
+ policies = result[:policies]&.size || 0
345
+ @output.puts " #{Theme.c(:success, 'ok')} #{name}: connected (#{policies} policies)"
346
+ end
347
+ end
348
+ @output.puts
349
+ sleep 1
350
+ end
351
+
158
352
  def build_onboarding_result(config, scan_data, github_data)
159
353
  {
160
354
  **config,
@@ -282,6 +476,85 @@ module Legion
282
476
  ['', "Running services: #{running.join(', ')}"]
283
477
  end
284
478
 
479
+ def dotfiles_summary_lines(scan_data)
480
+ return [] unless scan_data.is_a?(Hash)
481
+
482
+ dotfiles = scan_data[:dotfiles]
483
+ return [] unless dotfiles.is_a?(Hash)
484
+
485
+ lines = []
486
+ lines.concat(git_summary_lines(dotfiles[:git]))
487
+ lines.concat(jfrog_summary_lines(dotfiles[:jfrog]))
488
+ lines.concat(terraform_summary_lines(dotfiles[:terraform]))
489
+ lines
490
+ end
491
+
492
+ def git_summary_lines(git)
493
+ return [] unless git.is_a?(Hash)
494
+
495
+ lines = ['', "Git: #{git[:name]} <#{git[:email]}>"]
496
+ lines << " Signing key: #{git[:signing_key]}" if git[:signing_key]
497
+ lines
498
+ end
499
+
500
+ def jfrog_summary_lines(jfrog)
501
+ return [] unless jfrog.is_a?(Array) && !jfrog.empty?
502
+
503
+ lines = ['', 'JFrog Artifactory:']
504
+ jfrog.each { |s| lines << " #{s[:server_id]}: #{s[:url]} (#{s[:user]})" }
505
+ lines
506
+ end
507
+
508
+ def vault_summary_lines
509
+ return [] unless @vault_results.is_a?(Hash) && @vault_results.any?
510
+
511
+ lines = ['', 'Vault:']
512
+ @vault_results.each do |name, result|
513
+ lines << if result[:error]
514
+ " #{name}: failed (#{result[:error]})"
515
+ else
516
+ " #{name}: connected"
517
+ end
518
+ end
519
+ lines
520
+ end
521
+
522
+ def cache_summary_lines(scan_data)
523
+ services = scan_data.is_a?(Hash) ? scan_data[:services] : nil
524
+ return [] unless services.is_a?(Hash)
525
+
526
+ if services.dig(:memcached, :running)
527
+ ['', 'Memory: memcached online']
528
+ elsif services.dig(:redis, :running)
529
+ ['', 'Memory: redis online']
530
+ else
531
+ ['', 'Memory: no cache service running']
532
+ end
533
+ end
534
+
535
+ def bootstrap_summary_lines
536
+ return [] unless @bootstrap_data.is_a?(Hash)
537
+
538
+ sections = @bootstrap_data[:sections] || []
539
+ return [] if sections.empty?
540
+
541
+ ['', "Bootstrap config: #{sections.join(', ')}"]
542
+ end
543
+
544
+ def gaia_summary_lines
545
+ if legionio_running?
546
+ ['', 'GAIA: online']
547
+ else
548
+ ['', 'GAIA: dormant']
549
+ end
550
+ end
551
+
552
+ def terraform_summary_lines(dotfiles_tf)
553
+ return [] unless dotfiles_tf.is_a?(Hash) && dotfiles_tf[:hosts]&.any?
554
+
555
+ ['', "Terraform: #{dotfiles_tf[:hosts].join(', ')}"]
556
+ end
557
+
285
558
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
286
559
  def github_summary_lines(github_data)
287
560
  return [] unless github_data.is_a?(Hash)
@@ -443,6 +716,66 @@ module Legion
443
716
  nil
444
717
  end
445
718
 
719
+ def detect_cache_binary
720
+ if system('which memcached > /dev/null 2>&1')
721
+ :memcached
722
+ elsif system('which redis-server > /dev/null 2>&1')
723
+ :redis
724
+ end
725
+ end
726
+
727
+ def start_cache_service(service_name)
728
+ @log.log('cache', "starting #{service_name} via brew services")
729
+ result = system("brew services start #{service_name} > /dev/null 2>&1")
730
+ @log.log('cache', "brew services start #{service_name}: #{result ? 'ok' : 'failed'}")
731
+ result
732
+ rescue StandardError => e
733
+ @log.log('cache', "failed to start #{service_name}: #{e.message}")
734
+ false
735
+ end
736
+
737
+ def legionio_running?
738
+ pid_paths = [
739
+ File.expand_path('~/.legionio/legion.pid'),
740
+ '/tmp/legionio.pid'
741
+ ]
742
+ pid_paths.each do |path|
743
+ next unless File.exist?(path)
744
+
745
+ pid = File.read(path).strip.to_i
746
+ next unless pid.positive?
747
+
748
+ begin
749
+ ::Process.kill(0, pid)
750
+ return true
751
+ rescue Errno::ESRCH
752
+ next
753
+ rescue Errno::EPERM
754
+ return true
755
+ end
756
+ end
757
+ system('pgrep -x legionio > /dev/null 2>&1')
758
+ rescue StandardError
759
+ false
760
+ end
761
+
762
+ def start_legionio_daemon
763
+ @log.log('gaia', 'attempting to start legionio daemon')
764
+ if system('brew services start legionio > /dev/null 2>&1')
765
+ @log.log('gaia', 'started via brew services')
766
+ true
767
+ elsif system('legionio start -d > /dev/null 2>&1')
768
+ @log.log('gaia', 'started via legionio start -d')
769
+ true
770
+ else
771
+ @log.log('gaia', 'failed to start daemon')
772
+ false
773
+ end
774
+ rescue StandardError => e
775
+ @log.log('gaia', "start failed: #{e.message}")
776
+ false
777
+ end
778
+
446
779
  def terminal_width
447
780
  require 'tty-screen'
448
781
  ::TTY::Screen.width
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module TTY
5
- VERSION = '0.2.0'
5
+ VERSION = '0.2.5'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-tty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -195,8 +195,10 @@ files:
195
195
  - exe/legion-tty
196
196
  - lib/legion/tty.rb
197
197
  - lib/legion/tty/app.rb
198
+ - lib/legion/tty/background/bootstrap_config.rb
198
199
  - lib/legion/tty/background/github_probe.rb
199
200
  - lib/legion/tty/background/kerberos_probe.rb
201
+ - lib/legion/tty/background/llm_probe.rb
200
202
  - lib/legion/tty/background/scanner.rb
201
203
  - lib/legion/tty/boot_logger.rb
202
204
  - lib/legion/tty/components/digital_rain.rb