legion-tty 0.2.0
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 +7 -0
- data/CHANGELOG.md +32 -0
- data/LICENSE +201 -0
- data/README.md +138 -0
- data/exe/legion-tty +17 -0
- data/lib/legion/tty/app.rb +277 -0
- data/lib/legion/tty/background/github_probe.rb +357 -0
- data/lib/legion/tty/background/kerberos_probe.rb +205 -0
- data/lib/legion/tty/background/scanner.rb +160 -0
- data/lib/legion/tty/boot_logger.rb +34 -0
- data/lib/legion/tty/components/digital_rain.rb +138 -0
- data/lib/legion/tty/components/input_bar.rb +46 -0
- data/lib/legion/tty/components/markdown_view.rb +17 -0
- data/lib/legion/tty/components/message_stream.rb +86 -0
- data/lib/legion/tty/components/status_bar.rb +89 -0
- data/lib/legion/tty/components/token_tracker.rb +46 -0
- data/lib/legion/tty/components/tool_panel.rb +93 -0
- data/lib/legion/tty/components/wizard_prompt.rb +49 -0
- data/lib/legion/tty/hotkeys.rb +29 -0
- data/lib/legion/tty/screen_manager.rb +63 -0
- data/lib/legion/tty/screens/base.rb +28 -0
- data/lib/legion/tty/screens/chat.rb +428 -0
- data/lib/legion/tty/screens/dashboard.rb +211 -0
- data/lib/legion/tty/screens/onboarding.rb +463 -0
- data/lib/legion/tty/session_store.rb +74 -0
- data/lib/legion/tty/theme.rb +63 -0
- data/lib/legion/tty/version.rb +7 -0
- data/lib/legion/tty.rb +30 -0
- metadata +247 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../screens/base'
|
|
4
|
+
require_relative '../theme'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module TTY
|
|
8
|
+
module Screens
|
|
9
|
+
# rubocop:disable Metrics/ClassLength
|
|
10
|
+
class Dashboard < Base
|
|
11
|
+
def initialize(app)
|
|
12
|
+
super
|
|
13
|
+
@last_refresh = nil
|
|
14
|
+
@refresh_interval = 5
|
|
15
|
+
@cached_data = {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def activate
|
|
19
|
+
refresh_data
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# rubocop:disable Metrics/AbcSize
|
|
23
|
+
def render(width, height)
|
|
24
|
+
refresh_data if stale?
|
|
25
|
+
|
|
26
|
+
rows = []
|
|
27
|
+
rows.concat(render_header(width))
|
|
28
|
+
rows.concat(render_services_panel(width))
|
|
29
|
+
rows.concat(render_extensions_panel(width))
|
|
30
|
+
rows.concat(render_system_panel(width))
|
|
31
|
+
rows.concat(render_activity_panel(width, remaining_height(height, rows.size)))
|
|
32
|
+
rows.concat(render_help_bar(width))
|
|
33
|
+
|
|
34
|
+
pad_to_height(rows, height)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# rubocop:enable Metrics/AbcSize
|
|
38
|
+
|
|
39
|
+
def handle_input(key)
|
|
40
|
+
case key
|
|
41
|
+
when 'r', :f5
|
|
42
|
+
refresh_data
|
|
43
|
+
:handled
|
|
44
|
+
when 'q', :escape
|
|
45
|
+
:pop_screen
|
|
46
|
+
else
|
|
47
|
+
:pass
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def refresh_data
|
|
52
|
+
@last_refresh = Time.now
|
|
53
|
+
@cached_data = {
|
|
54
|
+
services: probe_services,
|
|
55
|
+
extensions: discover_extensions,
|
|
56
|
+
system: system_info,
|
|
57
|
+
activity: recent_activity
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def stale?
|
|
64
|
+
@last_refresh.nil? || (Time.now - @last_refresh) > @refresh_interval
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def render_header(width)
|
|
68
|
+
title = Theme.c(:primary, ' LEGION DASHBOARD ')
|
|
69
|
+
timestamp = Theme.c(:muted, @last_refresh&.strftime('%H:%M:%S') || '--:--:--')
|
|
70
|
+
line = "#{title}#{' ' * [width - 30, 0].max}#{timestamp}"
|
|
71
|
+
[line, Theme.c(:muted, '-' * width)]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def render_services_panel(_width)
|
|
75
|
+
services = @cached_data[:services] || {}
|
|
76
|
+
lines = [Theme.c(:accent, ' Services')]
|
|
77
|
+
services.each do |name, info|
|
|
78
|
+
icon = info[:running] ? Theme.c(:success, "\u2713") : Theme.c(:error, "\u2717")
|
|
79
|
+
port_str = Theme.c(:muted, ":#{info[:port]}")
|
|
80
|
+
lines << " #{icon} #{name} #{port_str}"
|
|
81
|
+
end
|
|
82
|
+
lines << ''
|
|
83
|
+
lines
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def render_extensions_panel(_width)
|
|
87
|
+
extensions = @cached_data[:extensions] || []
|
|
88
|
+
count = extensions.size
|
|
89
|
+
lines = [Theme.c(:accent, " Extensions (#{count})")]
|
|
90
|
+
if extensions.empty?
|
|
91
|
+
lines << Theme.c(:muted, ' No lex-* gems found')
|
|
92
|
+
else
|
|
93
|
+
extensions.first(8).each do |ext|
|
|
94
|
+
lines << " #{Theme.c(:secondary, ext)}"
|
|
95
|
+
end
|
|
96
|
+
remaining = count - 8
|
|
97
|
+
lines << Theme.c(:muted, " ... and #{remaining} more") if remaining.positive?
|
|
98
|
+
end
|
|
99
|
+
lines << ''
|
|
100
|
+
lines
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
104
|
+
def render_system_panel(_width)
|
|
105
|
+
sys = @cached_data[:system] || {}
|
|
106
|
+
lines = [Theme.c(:accent, ' System')]
|
|
107
|
+
lines << " Ruby: #{Theme.c(:secondary, sys[:ruby_version] || 'unknown')}"
|
|
108
|
+
lines << " OS: #{Theme.c(:secondary, sys[:os] || 'unknown')}"
|
|
109
|
+
lines << " Host: #{Theme.c(:secondary, sys[:hostname] || 'unknown')}"
|
|
110
|
+
lines << " PID: #{Theme.c(:secondary, sys[:pid]&.to_s || 'unknown')}"
|
|
111
|
+
lines << " Memory: #{Theme.c(:secondary, sys[:memory] || 'unknown')}"
|
|
112
|
+
lines << ''
|
|
113
|
+
lines
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
117
|
+
|
|
118
|
+
def render_activity_panel(_width, max_lines)
|
|
119
|
+
activity = @cached_data[:activity] || []
|
|
120
|
+
lines = [Theme.c(:accent, ' Recent Activity')]
|
|
121
|
+
if activity.empty?
|
|
122
|
+
lines << Theme.c(:muted, ' No recent activity')
|
|
123
|
+
else
|
|
124
|
+
available = [max_lines - 2, 1].max
|
|
125
|
+
activity.last(available).each do |entry|
|
|
126
|
+
lines << " #{Theme.c(:muted, entry)}"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
lines << ''
|
|
130
|
+
lines
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def render_help_bar(width)
|
|
134
|
+
help = " #{Theme.c(:muted, 'r')}=refresh #{Theme.c(:muted, 'q')}=back " \
|
|
135
|
+
"#{Theme.c(:muted, 'Ctrl+D')}=dashboard #{Theme.c(:muted, 'Ctrl+C')}=quit"
|
|
136
|
+
[Theme.c(:muted, '-' * width), help]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def remaining_height(total, used)
|
|
140
|
+
[total - used - 4, 3].max
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def pad_to_height(rows, height)
|
|
144
|
+
if rows.size < height
|
|
145
|
+
rows + Array.new(height - rows.size, '')
|
|
146
|
+
else
|
|
147
|
+
rows.first(height)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def probe_services
|
|
152
|
+
require 'socket'
|
|
153
|
+
{
|
|
154
|
+
rabbitmq: { port: 5672, running: port_open?(5672) },
|
|
155
|
+
redis: { port: 6379, running: port_open?(6379) },
|
|
156
|
+
memcached: { port: 11_211, running: port_open?(11_211) },
|
|
157
|
+
vault: { port: 8200, running: port_open?(8200) },
|
|
158
|
+
postgres: { port: 5432, running: port_open?(5432) }
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def port_open?(port)
|
|
163
|
+
::Socket.tcp('127.0.0.1', port, connect_timeout: 0.5) { true }
|
|
164
|
+
rescue StandardError
|
|
165
|
+
false
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def discover_extensions
|
|
169
|
+
Gem::Specification.select { |s| s.name.start_with?('lex-') }.map(&:name).sort
|
|
170
|
+
rescue StandardError
|
|
171
|
+
[]
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def system_info
|
|
175
|
+
{
|
|
176
|
+
ruby_version: RUBY_VERSION,
|
|
177
|
+
os: RUBY_PLATFORM,
|
|
178
|
+
hostname: ::Socket.gethostname,
|
|
179
|
+
pid: ::Process.pid,
|
|
180
|
+
memory: format_memory
|
|
181
|
+
}
|
|
182
|
+
rescue StandardError
|
|
183
|
+
{}
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def format_memory
|
|
187
|
+
if RUBY_PLATFORM.include?('darwin')
|
|
188
|
+
rss = `ps -o rss= -p #{::Process.pid} 2>/dev/null`.strip.to_i
|
|
189
|
+
"#{(rss / 1024.0).round(1)} MB"
|
|
190
|
+
else
|
|
191
|
+
match = `cat /proc/#{::Process.pid}/status 2>/dev/null`.match(/VmRSS:\s+(\d+)/)
|
|
192
|
+
rss_kb = match ? match[1].to_i : 0
|
|
193
|
+
"#{(rss_kb / 1024.0).round(1)} MB"
|
|
194
|
+
end
|
|
195
|
+
rescue StandardError
|
|
196
|
+
'unknown'
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def recent_activity
|
|
200
|
+
log_path = File.expand_path('~/.legionio/logs/tty-boot.log')
|
|
201
|
+
return [] unless File.exist?(log_path)
|
|
202
|
+
|
|
203
|
+
File.readlines(log_path, chomp: true).last(20)
|
|
204
|
+
rescue StandardError
|
|
205
|
+
[]
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
# rubocop:enable Metrics/ClassLength
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../screens/base'
|
|
4
|
+
require_relative '../components/digital_rain'
|
|
5
|
+
require_relative '../components/wizard_prompt'
|
|
6
|
+
require_relative '../background/scanner'
|
|
7
|
+
require_relative '../background/github_probe'
|
|
8
|
+
require_relative '../background/kerberos_probe'
|
|
9
|
+
require_relative '../boot_logger'
|
|
10
|
+
require_relative '../theme'
|
|
11
|
+
|
|
12
|
+
module Legion
|
|
13
|
+
module TTY
|
|
14
|
+
module Screens
|
|
15
|
+
# rubocop:disable Metrics/ClassLength
|
|
16
|
+
class Onboarding < Base
|
|
17
|
+
TYPED_DELAY = 0.05
|
|
18
|
+
|
|
19
|
+
def initialize(app, wizard: nil, output: $stdout, skip_rain: false)
|
|
20
|
+
super(app)
|
|
21
|
+
@wizard = wizard || Components::WizardPrompt.new
|
|
22
|
+
@output = output
|
|
23
|
+
@skip_rain = skip_rain
|
|
24
|
+
@scan_queue = Queue.new
|
|
25
|
+
@github_queue = Queue.new
|
|
26
|
+
@github_quick_queue = Queue.new
|
|
27
|
+
@kerberos_queue = Queue.new
|
|
28
|
+
@kerberos_identity = nil
|
|
29
|
+
@github_quick = nil
|
|
30
|
+
@log = BootLogger.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def activate
|
|
34
|
+
@log.log('onboarding', 'activate started')
|
|
35
|
+
start_background_threads
|
|
36
|
+
run_rain unless @skip_rain
|
|
37
|
+
run_intro
|
|
38
|
+
config = run_wizard
|
|
39
|
+
@log.log('wizard', "name=#{config[:name]} provider=#{config[:provider]}")
|
|
40
|
+
scan_data, github_data = collect_background_results
|
|
41
|
+
run_reveal(name: config[:name], scan_data: scan_data, github_data: github_data)
|
|
42
|
+
@log.log('onboarding', 'activate complete')
|
|
43
|
+
build_onboarding_result(config, scan_data, github_data)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# rubocop:disable Metrics/AbcSize
|
|
47
|
+
def run_rain
|
|
48
|
+
require 'tty-cursor'
|
|
49
|
+
require 'tty-font'
|
|
50
|
+
width = terminal_width
|
|
51
|
+
height = terminal_height
|
|
52
|
+
rain = Components::DigitalRain.new(width: width, height: height)
|
|
53
|
+
rain.run(duration_seconds: 20, fps: 18, output: @output)
|
|
54
|
+
@output.print ::TTY::Cursor.clear_screen
|
|
55
|
+
font = ::TTY::Font.new(:standard)
|
|
56
|
+
title = font.write('LEGION')
|
|
57
|
+
title.each_line do |line|
|
|
58
|
+
@output.puts line.center(width)
|
|
59
|
+
end
|
|
60
|
+
@output.puts Theme.c(:muted, 'async cognition engine').center(width + 20)
|
|
61
|
+
sleep 5
|
|
62
|
+
@output.print ::TTY::Cursor.clear_screen
|
|
63
|
+
end
|
|
64
|
+
# rubocop:enable Metrics/AbcSize
|
|
65
|
+
|
|
66
|
+
def run_intro
|
|
67
|
+
# Collect background results that ran during the rain
|
|
68
|
+
collect_kerberos_identity
|
|
69
|
+
collect_github_quick
|
|
70
|
+
|
|
71
|
+
sleep 2
|
|
72
|
+
typed_output('...')
|
|
73
|
+
sleep 1.2
|
|
74
|
+
@output.puts
|
|
75
|
+
@output.puts
|
|
76
|
+
typed_output("Hello. I'm Legion.")
|
|
77
|
+
@output.puts
|
|
78
|
+
sleep 1.5
|
|
79
|
+
if @kerberos_identity
|
|
80
|
+
run_intro_with_identity
|
|
81
|
+
else
|
|
82
|
+
typed_output("Let's get you set up.")
|
|
83
|
+
@output.puts
|
|
84
|
+
@output.puts
|
|
85
|
+
end
|
|
86
|
+
run_intro_with_github if @github_quick
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def run_wizard
|
|
90
|
+
name = ask_for_name
|
|
91
|
+
sleep 0.8
|
|
92
|
+
typed_output(" Nice to meet you, #{name}.")
|
|
93
|
+
@output.puts
|
|
94
|
+
sleep 1
|
|
95
|
+
typed_output("Let's get you connected.")
|
|
96
|
+
@output.puts
|
|
97
|
+
@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 }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def start_background_threads
|
|
105
|
+
@log.log('threads', 'launching scanner, kerberos probe, github quick probe')
|
|
106
|
+
@scanner = Background::Scanner.new(logger: @log)
|
|
107
|
+
@github_probe = Background::GitHubProbe.new(logger: @log)
|
|
108
|
+
@kerberos_probe = Background::KerberosProbe.new(logger: @log)
|
|
109
|
+
@scanner.run_async(@scan_queue)
|
|
110
|
+
@kerberos_probe.run_async(@kerberos_queue)
|
|
111
|
+
@github_probe.run_quick_async(@github_quick_queue)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def collect_background_results
|
|
115
|
+
@log.log('collect', 'waiting for scanner results (10s timeout)')
|
|
116
|
+
scan_result = drain_with_timeout(@scan_queue, timeout: 10)
|
|
117
|
+
scan_data = scan_result&.dig(:data) || { services: {}, repos: [], tools: {} }
|
|
118
|
+
log_scan_data(scan_data)
|
|
119
|
+
|
|
120
|
+
# Now launch GitHub probe with discovered remotes
|
|
121
|
+
remotes = scan_data[:repos]&.filter_map { |r| r[:remote] } || []
|
|
122
|
+
@log.log('collect', "launching github probe with #{remotes.size} remotes")
|
|
123
|
+
@github_probe.run_async(@github_queue, remotes: remotes, quick_profile: @github_quick)
|
|
124
|
+
github_result = drain_with_timeout(@github_queue, timeout: 8)
|
|
125
|
+
github_data = github_result&.dig(:data)
|
|
126
|
+
log_github_data(github_data)
|
|
127
|
+
[scan_data, github_data]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def run_reveal(name:, scan_data:, github_data:)
|
|
131
|
+
require 'tty-box'
|
|
132
|
+
@output.puts
|
|
133
|
+
typed_output('One moment...')
|
|
134
|
+
@output.puts
|
|
135
|
+
sleep 1.5
|
|
136
|
+
summary = build_summary(name: name, scan_data: scan_data, github_data: github_data)
|
|
137
|
+
box = ::TTY::Box.frame(summary, padding: 1, border: :thick)
|
|
138
|
+
@output.puts box
|
|
139
|
+
@output.puts
|
|
140
|
+
@wizard.confirm('Does this look right?')
|
|
141
|
+
@output.puts
|
|
142
|
+
sleep 0.8
|
|
143
|
+
typed_output("Let's chat.")
|
|
144
|
+
@output.puts
|
|
145
|
+
sleep 1
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def build_summary(name:, scan_data:, github_data:)
|
|
149
|
+
lines = ["Hello, #{name}!", '', "Here's what I found:"]
|
|
150
|
+
lines.concat(identity_summary_lines)
|
|
151
|
+
lines.concat(scan_summary_lines(scan_data))
|
|
152
|
+
lines.concat(github_summary_lines(github_data))
|
|
153
|
+
lines.join("\n")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
private
|
|
157
|
+
|
|
158
|
+
def build_onboarding_result(config, scan_data, github_data)
|
|
159
|
+
{
|
|
160
|
+
**config,
|
|
161
|
+
identity: @kerberos_identity,
|
|
162
|
+
github: github_data,
|
|
163
|
+
scan: scan_data
|
|
164
|
+
}
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def collect_github_quick
|
|
168
|
+
@log.log('github', 'collecting quick profile (3s timeout)')
|
|
169
|
+
result = drain_with_timeout(@github_quick_queue, timeout: 3)
|
|
170
|
+
@github_quick = result&.dig(:data)
|
|
171
|
+
if @github_quick
|
|
172
|
+
@log.log('github', "quick profile: #{@github_quick[:username]} " \
|
|
173
|
+
"commits_week=#{@github_quick[:commits_this_week]} " \
|
|
174
|
+
"commits_month=#{@github_quick[:commits_this_month]}")
|
|
175
|
+
else
|
|
176
|
+
@log.log('github', 'no quick profile available')
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
181
|
+
def run_intro_with_github
|
|
182
|
+
gh = @github_quick
|
|
183
|
+
name = gh[:name] || gh[:username]
|
|
184
|
+
|
|
185
|
+
if gh[:commits_this_week]&.positive?
|
|
186
|
+
typed_output("#{gh[:commits_this_week]} commits this week, #{name}. You've been busy.")
|
|
187
|
+
@output.puts
|
|
188
|
+
sleep 1
|
|
189
|
+
elsif gh[:commits_this_month]&.positive?
|
|
190
|
+
typed_output("#{gh[:commits_this_month]} commits this month across GitHub.")
|
|
191
|
+
@output.puts
|
|
192
|
+
sleep 1
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
total = (gh[:public_repos] || 0) + (gh[:private_repos] || 0)
|
|
196
|
+
if total.positive?
|
|
197
|
+
typed_output("#{total} repositories. I can work with that.")
|
|
198
|
+
@output.puts
|
|
199
|
+
sleep 0.8
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
@output.puts
|
|
203
|
+
end
|
|
204
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
205
|
+
|
|
206
|
+
def collect_kerberos_identity
|
|
207
|
+
@log.log('kerberos', 'collecting identity (2s timeout)')
|
|
208
|
+
result = drain_with_timeout(@kerberos_queue, timeout: 2)
|
|
209
|
+
@kerberos_identity = result&.dig(:data)
|
|
210
|
+
if @kerberos_identity
|
|
211
|
+
@log.log_hash('kerberos', 'identity found', @kerberos_identity)
|
|
212
|
+
else
|
|
213
|
+
@log.log('kerberos', 'no identity found (klist failed or no ticket)')
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
218
|
+
def run_intro_with_identity
|
|
219
|
+
id = @kerberos_identity
|
|
220
|
+
typed_output("I see you, #{id[:first_name]}.")
|
|
221
|
+
@output.puts
|
|
222
|
+
sleep 1.2
|
|
223
|
+
|
|
224
|
+
if id[:title]
|
|
225
|
+
typed_output("#{id[:title]}, #{id[:company] || id[:department]}")
|
|
226
|
+
@output.puts
|
|
227
|
+
sleep 0.8
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
if id[:city] && id[:state]
|
|
231
|
+
typed_output("#{id[:city]}, #{id[:state]}")
|
|
232
|
+
@output.puts
|
|
233
|
+
sleep 0.8
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
if id[:tenure_years]
|
|
237
|
+
typed_output("#{format_tenure(id[:tenure_years])} and counting.")
|
|
238
|
+
@output.puts
|
|
239
|
+
sleep 1
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
@output.puts
|
|
243
|
+
typed_output("I've been looking forward to meeting you.")
|
|
244
|
+
@output.puts
|
|
245
|
+
@output.puts
|
|
246
|
+
end
|
|
247
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
248
|
+
|
|
249
|
+
def ask_for_name
|
|
250
|
+
if @kerberos_identity
|
|
251
|
+
@wizard.ask_name_with_default(@kerberos_identity[:first_name])
|
|
252
|
+
else
|
|
253
|
+
@wizard.ask_name
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
258
|
+
def identity_summary_lines
|
|
259
|
+
return [] unless @kerberos_identity
|
|
260
|
+
|
|
261
|
+
id = @kerberos_identity
|
|
262
|
+
lines = ['']
|
|
263
|
+
lines << "Identity: #{id[:display_name]} (#{id[:principal]})"
|
|
264
|
+
lines << " Title: #{id[:title]}" if id[:title]
|
|
265
|
+
lines << " Org: #{[id[:department], id[:company]].compact.join(' / ')}" if id[:department] || id[:company]
|
|
266
|
+
lines << " Location: #{[id[:city], id[:state], id[:country]].compact.join(', ')}" if id[:city]
|
|
267
|
+
lines << " Tenure: #{format_tenure(id[:tenure_years])}" if id[:tenure_years]
|
|
268
|
+
lines << " Email: #{id[:email]}" if id[:email]
|
|
269
|
+
lines
|
|
270
|
+
end
|
|
271
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
272
|
+
|
|
273
|
+
def scan_summary_lines(scan_data)
|
|
274
|
+
return [] unless scan_data.is_a?(Hash)
|
|
275
|
+
|
|
276
|
+
services = scan_data[:services]
|
|
277
|
+
return [] unless services.is_a?(Hash)
|
|
278
|
+
|
|
279
|
+
running = services.values.select { |s| s[:running] }.map { |s| s[:name] }
|
|
280
|
+
return [] if running.empty?
|
|
281
|
+
|
|
282
|
+
['', "Running services: #{running.join(', ')}"]
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
|
|
286
|
+
def github_summary_lines(github_data)
|
|
287
|
+
return [] unless github_data.is_a?(Hash)
|
|
288
|
+
|
|
289
|
+
username = github_data[:username]
|
|
290
|
+
return [] unless username
|
|
291
|
+
|
|
292
|
+
lines = ['', "GitHub: #{username}"]
|
|
293
|
+
profile = github_data[:profile]
|
|
294
|
+
if profile.is_a?(Hash)
|
|
295
|
+
lines << " Name: #{profile[:name]}" if profile[:name]
|
|
296
|
+
lines << " Company: #{profile[:company]}" if profile[:company]
|
|
297
|
+
lines << " Public repos: #{profile[:public_repos]}" if profile[:public_repos]
|
|
298
|
+
lines << " Private repos: #{profile[:private_repos]}" if profile[:private_repos]
|
|
299
|
+
lines << " Followers: #{profile[:followers]}" if profile[:followers]
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
orgs = github_data[:orgs]
|
|
303
|
+
lines << " Orgs: #{orgs.map { |o| o[:login] }.join(', ')}" if orgs.is_a?(Array) && !orgs.empty?
|
|
304
|
+
|
|
305
|
+
prs = github_data[:recent_prs]
|
|
306
|
+
if prs.is_a?(Array) && !prs.empty?
|
|
307
|
+
lines << ' Recent PRs:'
|
|
308
|
+
prs.first(3).each do |pr|
|
|
309
|
+
lines << " #{pr[:state] == 'open' ? '○' : '●'} #{pr[:repo]}: #{pr[:title]}"
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
notifs = github_data[:notifications]
|
|
314
|
+
lines << " Notifications: #{notifs.size} unread" if notifs.is_a?(Array) && !notifs.empty?
|
|
315
|
+
|
|
316
|
+
lines
|
|
317
|
+
end
|
|
318
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
|
|
319
|
+
|
|
320
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
321
|
+
def format_tenure(tenure)
|
|
322
|
+
return tenure.to_s unless tenure.is_a?(Hash)
|
|
323
|
+
|
|
324
|
+
parts = []
|
|
325
|
+
y = tenure[:years]
|
|
326
|
+
m = tenure[:months]
|
|
327
|
+
d = tenure[:days]
|
|
328
|
+
parts << "#{y} year#{'s' if y != 1}" if y&.positive?
|
|
329
|
+
parts << "#{m} month#{'s' if m != 1}" if m&.positive?
|
|
330
|
+
parts << "#{d} day#{'s' if d != 1}" if d&.positive?
|
|
331
|
+
parts.join(', ')
|
|
332
|
+
end
|
|
333
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
334
|
+
|
|
335
|
+
def typed_output(text, delay: TYPED_DELAY)
|
|
336
|
+
text.chars.each do |char|
|
|
337
|
+
@output.print Theme.c(:primary, char)
|
|
338
|
+
@output.flush if @output.respond_to?(:flush)
|
|
339
|
+
sleep delay
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
344
|
+
def log_scan_data(scan_data)
|
|
345
|
+
services = scan_data[:services] || {}
|
|
346
|
+
running = services.values.select { |s| s[:running] }.map { |s| s[:name] }
|
|
347
|
+
stopped = services.values.reject { |s| s[:running] }.map { |s| s[:name] }
|
|
348
|
+
@log.log('scanner', "services running: #{running.join(', ').then { |s| s.empty? ? 'none' : s }}")
|
|
349
|
+
@log.log('scanner', "services stopped: #{stopped.join(', ').then { |s| s.empty? ? 'none' : s }}")
|
|
350
|
+
|
|
351
|
+
repos = scan_data[:repos] || []
|
|
352
|
+
@log.log('scanner', "git repos found: #{repos.size}")
|
|
353
|
+
repos.each do |r|
|
|
354
|
+
@log.log('scanner', " repo: #{r[:name]} branch=#{r[:branch]} lang=#{r[:language]} remote=#{r[:remote]}")
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
tools = scan_data[:tools] || {}
|
|
358
|
+
@log.log('scanner', "top shell commands: #{tools.first(10).map { |k, v| "#{k}(#{v})" }.join(', ')}")
|
|
359
|
+
|
|
360
|
+
configs = scan_data[:configs] || []
|
|
361
|
+
@log.log('scanner', "config files: #{configs.join(', ').then { |s| s.empty? ? 'none' : s }}")
|
|
362
|
+
end
|
|
363
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
364
|
+
|
|
365
|
+
# rubocop:disable Metrics/AbcSize
|
|
366
|
+
def log_github_data(github_data)
|
|
367
|
+
unless github_data.is_a?(Hash)
|
|
368
|
+
@log.log('github', 'no data returned')
|
|
369
|
+
return
|
|
370
|
+
end
|
|
371
|
+
@log.log('github', "username: #{github_data[:username] || 'unknown'}")
|
|
372
|
+
@log.log('github', "authenticated: #{github_data[:authenticated]}")
|
|
373
|
+
|
|
374
|
+
profile = github_data[:profile]
|
|
375
|
+
if profile.is_a?(Hash)
|
|
376
|
+
@log.log('github', " name: #{profile[:name]}")
|
|
377
|
+
@log.log('github', " email: #{profile[:email]}")
|
|
378
|
+
@log.log('github', " company: #{profile[:company]}")
|
|
379
|
+
@log.log('github', " location: #{profile[:location]}")
|
|
380
|
+
@log.log('github', " public_repos: #{profile[:public_repos]}")
|
|
381
|
+
@log.log('github', " private_repos: #{profile[:private_repos]}")
|
|
382
|
+
@log.log('github', " followers: #{profile[:followers]} following: #{profile[:following]}")
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
orgs = github_data[:orgs] || []
|
|
386
|
+
@log.log('github', "orgs: #{orgs.map { |o| o[:login] }.join(', ').then { |s| s.empty? ? 'none' : s }}")
|
|
387
|
+
|
|
388
|
+
log_github_repos(github_data)
|
|
389
|
+
log_github_activity(github_data)
|
|
390
|
+
end
|
|
391
|
+
# rubocop:enable Metrics/AbcSize
|
|
392
|
+
|
|
393
|
+
# rubocop:disable Metrics/AbcSize
|
|
394
|
+
def log_github_repos(github_data)
|
|
395
|
+
public_repos = github_data[:public_repos] || []
|
|
396
|
+
@log.log('github', "public repos: #{public_repos.size}")
|
|
397
|
+
public_repos.first(5).each do |r|
|
|
398
|
+
@log.log('github', " #{r[:full_name]} (#{r[:language]}) updated=#{r[:updated_at]}")
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
private_repos = github_data[:private_repos] || []
|
|
402
|
+
@log.log('github', "private repos: #{private_repos.size}")
|
|
403
|
+
private_repos.first(5).each do |r|
|
|
404
|
+
@log.log('github', " #{r[:full_name]} (#{r[:language]}) updated=#{r[:updated_at]}")
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
starred = github_data[:starred] || []
|
|
408
|
+
@log.log('github', "recently starred: #{starred.size}")
|
|
409
|
+
end
|
|
410
|
+
# rubocop:enable Metrics/AbcSize
|
|
411
|
+
|
|
412
|
+
# rubocop:disable Metrics/AbcSize
|
|
413
|
+
def log_github_activity(github_data)
|
|
414
|
+
prs = github_data[:recent_prs] || []
|
|
415
|
+
@log.log('github', "recent PRs: #{prs.size}")
|
|
416
|
+
prs.first(5).each do |pr|
|
|
417
|
+
@log.log('github', " [#{pr[:state]}] #{pr[:repo]}: #{pr[:title]}")
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
events = github_data[:events] || []
|
|
421
|
+
@log.log('github', "recent events: #{events.size}")
|
|
422
|
+
events.first(5).each do |e|
|
|
423
|
+
@log.log('github', " #{e[:type]} on #{e[:repo]} at #{e[:created_at]}")
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
notifs = github_data[:notifications] || []
|
|
427
|
+
@log.log('github', "notifications: #{notifs.size}")
|
|
428
|
+
notifs.first(5).each do |n|
|
|
429
|
+
@log.log('github', " [#{n[:reason]}] #{n[:repo]}: #{n[:title]}")
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
# rubocop:enable Metrics/AbcSize
|
|
433
|
+
|
|
434
|
+
def drain_with_timeout(queue, timeout:)
|
|
435
|
+
deadline = Time.now + timeout
|
|
436
|
+
loop do
|
|
437
|
+
return queue.pop(true) unless queue.empty?
|
|
438
|
+
return nil if Time.now >= deadline
|
|
439
|
+
|
|
440
|
+
sleep 0.1
|
|
441
|
+
end
|
|
442
|
+
rescue ThreadError
|
|
443
|
+
nil
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def terminal_width
|
|
447
|
+
require 'tty-screen'
|
|
448
|
+
::TTY::Screen.width
|
|
449
|
+
rescue StandardError
|
|
450
|
+
80
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def terminal_height
|
|
454
|
+
require 'tty-screen'
|
|
455
|
+
::TTY::Screen.height
|
|
456
|
+
rescue StandardError
|
|
457
|
+
24
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
# rubocop:enable Metrics/ClassLength
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
end
|