archsight 0.1.2 → 0.1.3
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 +4 -4
- data/README.md +26 -5
- data/lib/archsight/analysis/executor.rb +112 -0
- data/lib/archsight/analysis/result.rb +174 -0
- data/lib/archsight/analysis/sandbox.rb +319 -0
- data/lib/archsight/analysis.rb +11 -0
- data/lib/archsight/annotations/architecture_annotations.rb +2 -2
- data/lib/archsight/cli.rb +163 -0
- data/lib/archsight/database.rb +6 -2
- data/lib/archsight/helpers/analysis_renderer.rb +83 -0
- data/lib/archsight/helpers/formatting.rb +95 -0
- data/lib/archsight/helpers.rb +20 -4
- data/lib/archsight/import/concurrent_progress.rb +341 -0
- data/lib/archsight/import/executor.rb +466 -0
- data/lib/archsight/import/git_analytics.rb +626 -0
- data/lib/archsight/import/handler.rb +263 -0
- data/lib/archsight/import/handlers/github.rb +161 -0
- data/lib/archsight/import/handlers/gitlab.rb +202 -0
- data/lib/archsight/import/handlers/jira_base.rb +189 -0
- data/lib/archsight/import/handlers/jira_discover.rb +161 -0
- data/lib/archsight/import/handlers/jira_metrics.rb +179 -0
- data/lib/archsight/import/handlers/openapi_schema_parser.rb +279 -0
- data/lib/archsight/import/handlers/repository.rb +439 -0
- data/lib/archsight/import/handlers/rest_api.rb +293 -0
- data/lib/archsight/import/handlers/rest_api_index.rb +183 -0
- data/lib/archsight/import/progress.rb +91 -0
- data/lib/archsight/import/registry.rb +54 -0
- data/lib/archsight/import/shared_file_writer.rb +67 -0
- data/lib/archsight/import/team_matcher.rb +195 -0
- data/lib/archsight/import.rb +14 -0
- data/lib/archsight/resources/analysis.rb +91 -0
- data/lib/archsight/resources/application_component.rb +2 -2
- data/lib/archsight/resources/application_service.rb +12 -12
- data/lib/archsight/resources/business_product.rb +12 -12
- data/lib/archsight/resources/data_object.rb +1 -1
- data/lib/archsight/resources/import.rb +79 -0
- data/lib/archsight/resources/technology_artifact.rb +23 -2
- data/lib/archsight/version.rb +1 -1
- data/lib/archsight/web/api/docs.rb +17 -0
- data/lib/archsight/web/api/json_helpers.rb +164 -0
- data/lib/archsight/web/api/openapi/spec.yaml +500 -0
- data/lib/archsight/web/api/routes.rb +101 -0
- data/lib/archsight/web/application.rb +66 -43
- data/lib/archsight/web/doc/import.md +458 -0
- data/lib/archsight/web/doc/index.md.erb +1 -0
- data/lib/archsight/web/public/css/artifact.css +10 -0
- data/lib/archsight/web/public/css/graph.css +14 -0
- data/lib/archsight/web/public/css/instance.css +489 -0
- data/lib/archsight/web/views/api_docs.erb +19 -0
- data/lib/archsight/web/views/partials/artifact/_project_estimate.haml +14 -8
- data/lib/archsight/web/views/partials/instance/_analysis_detail.haml +74 -0
- data/lib/archsight/web/views/partials/instance/_analysis_result.haml +64 -0
- data/lib/archsight/web/views/partials/instance/_detail.haml +7 -3
- data/lib/archsight/web/views/partials/instance/_import_detail.haml +87 -0
- data/lib/archsight/web/views/partials/instance/_relations.haml +4 -4
- data/lib/archsight/web/views/partials/layout/_content.haml +4 -0
- data/lib/archsight/web/views/partials/layout/_navigation.haml +6 -5
- metadata +78 -1
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Manages concurrent progress output with slot-based display
|
|
4
|
+
#
|
|
5
|
+
# In TTY mode: Each slot gets its own line, updated in place using ANSI codes with colors
|
|
6
|
+
# In non-TTY mode: Each update prints on its own line with context prefix (no colors)
|
|
7
|
+
class Archsight::Import::ConcurrentProgress
|
|
8
|
+
# ANSI color codes
|
|
9
|
+
COLORS = {
|
|
10
|
+
reset: "\e[0m",
|
|
11
|
+
bold: "\e[1m",
|
|
12
|
+
dim: "\e[2m",
|
|
13
|
+
green: "\e[32m",
|
|
14
|
+
yellow: "\e[33m",
|
|
15
|
+
blue: "\e[34m",
|
|
16
|
+
magenta: "\e[35m",
|
|
17
|
+
cyan: "\e[36m",
|
|
18
|
+
red: "\e[31m"
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
# ANSI cursor control
|
|
22
|
+
CURSOR_HIDE = "\e[?25l"
|
|
23
|
+
CURSOR_SHOW = "\e[?25h"
|
|
24
|
+
CURSOR_SAVE = "\e[s"
|
|
25
|
+
CURSOR_RESTORE = "\e[u"
|
|
26
|
+
|
|
27
|
+
def initialize(max_slots:, output: $stdout)
|
|
28
|
+
@output = output
|
|
29
|
+
@tty = output.respond_to?(:tty?) && output.tty?
|
|
30
|
+
@max_slots = max_slots
|
|
31
|
+
@mutex = Mutex.new
|
|
32
|
+
@slots = {}
|
|
33
|
+
@slot_queue = Queue.new
|
|
34
|
+
@lines_printed = 0
|
|
35
|
+
|
|
36
|
+
# Overall progress tracking
|
|
37
|
+
@total_imports = 0
|
|
38
|
+
@completed_imports = 0
|
|
39
|
+
@has_overall_line = false
|
|
40
|
+
@start_time = nil
|
|
41
|
+
|
|
42
|
+
# Initialize slot queue
|
|
43
|
+
max_slots.times { |i| @slot_queue << i }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def tty?
|
|
47
|
+
@tty
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Initialize total number of imports for overall progress
|
|
51
|
+
def total=(total)
|
|
52
|
+
@mutex.synchronize do
|
|
53
|
+
@total_imports = total
|
|
54
|
+
@completed_imports = 0
|
|
55
|
+
@start_time = Time.now
|
|
56
|
+
if @tty && !@has_overall_line
|
|
57
|
+
# Save cursor position and hide cursor for clean display
|
|
58
|
+
@output.print CURSOR_SAVE
|
|
59
|
+
@output.print CURSOR_HIDE
|
|
60
|
+
@output.puts build_overall_line
|
|
61
|
+
@has_overall_line = true
|
|
62
|
+
@lines_printed += 1
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Update total without resetting completed count (for multi-stage imports)
|
|
68
|
+
def update_total(total)
|
|
69
|
+
@mutex.synchronize do
|
|
70
|
+
@total_imports = total
|
|
71
|
+
update_overall_line if @tty && @has_overall_line
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Increment completed count and update overall progress
|
|
76
|
+
def increment_completed
|
|
77
|
+
@mutex.synchronize do
|
|
78
|
+
@completed_imports += 1
|
|
79
|
+
update_overall_line if @tty && @has_overall_line
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Acquire a slot for a new task
|
|
84
|
+
# @return [SlotProgress] A progress reporter for this slot
|
|
85
|
+
def acquire_slot(context)
|
|
86
|
+
slot_id = @slot_queue.pop
|
|
87
|
+
slot = SlotProgress.new(self, slot_id, context)
|
|
88
|
+
|
|
89
|
+
@mutex.synchronize do
|
|
90
|
+
@slots[slot_id] = slot
|
|
91
|
+
# Slot lines start after the overall progress line (if present)
|
|
92
|
+
effective_line = @has_overall_line ? slot_id + 1 : slot_id
|
|
93
|
+
if @tty && effective_line >= @lines_printed
|
|
94
|
+
# Print empty lines to reserve space
|
|
95
|
+
(@lines_printed..effective_line).each { @output.puts }
|
|
96
|
+
@lines_printed = effective_line + 1
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
slot
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Release a slot back to the pool
|
|
104
|
+
def release_slot(slot_id)
|
|
105
|
+
@mutex.synchronize do
|
|
106
|
+
@slots.delete(slot_id)
|
|
107
|
+
end
|
|
108
|
+
@slot_queue << slot_id
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Update a specific slot's display
|
|
112
|
+
def update_slot(slot_id, context, message, current: nil, total: nil, color: nil)
|
|
113
|
+
line = build_line(context, message, current, total, color: color)
|
|
114
|
+
|
|
115
|
+
@mutex.synchronize do
|
|
116
|
+
if @tty
|
|
117
|
+
# Move cursor to slot line and update (account for overall progress line)
|
|
118
|
+
effective_line = @has_overall_line ? slot_id + 1 : slot_id
|
|
119
|
+
lines_up = @lines_printed - effective_line
|
|
120
|
+
@output.print "\e[#{lines_up}A" # Move up
|
|
121
|
+
@output.print "\e[2K" # Clear line
|
|
122
|
+
@output.print line
|
|
123
|
+
@output.print "\e[#{lines_up}B" # Move back down
|
|
124
|
+
@output.print "\r" # Return to start of line
|
|
125
|
+
@output.flush
|
|
126
|
+
else
|
|
127
|
+
@output.puts line
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Mark a slot as complete
|
|
133
|
+
def complete_slot(slot_id, context, message = nil)
|
|
134
|
+
@mutex.synchronize do
|
|
135
|
+
if @tty
|
|
136
|
+
effective_line = @has_overall_line ? slot_id + 1 : slot_id
|
|
137
|
+
lines_up = @lines_printed - effective_line
|
|
138
|
+
@output.print "\e[#{lines_up}A"
|
|
139
|
+
@output.print "\e[2K"
|
|
140
|
+
msg = message || "Done"
|
|
141
|
+
@output.print "#{COLORS[:bold]}#{context}#{COLORS[:reset]} - #{COLORS[:green]}#{msg}#{COLORS[:reset]}"
|
|
142
|
+
@output.print "\e[#{lines_up}B"
|
|
143
|
+
@output.print "\r"
|
|
144
|
+
@output.flush
|
|
145
|
+
elsif message
|
|
146
|
+
@output.puts "#{context} - #{message}"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Report an error for a slot
|
|
152
|
+
def error_slot(slot_id, context, message)
|
|
153
|
+
safe_message = sanitize_message(message)
|
|
154
|
+
@mutex.synchronize do
|
|
155
|
+
if @tty
|
|
156
|
+
effective_line = @has_overall_line ? slot_id + 1 : slot_id
|
|
157
|
+
lines_up = @lines_printed - effective_line
|
|
158
|
+
@output.print "\e[#{lines_up}A"
|
|
159
|
+
@output.print "\e[2K"
|
|
160
|
+
@output.print "#{COLORS[:bold]}#{context}#{COLORS[:reset]} - #{COLORS[:red]}Error: #{safe_message}#{COLORS[:reset]}"
|
|
161
|
+
@output.print "\e[#{lines_up}B"
|
|
162
|
+
@output.print "\r"
|
|
163
|
+
@output.flush
|
|
164
|
+
else
|
|
165
|
+
@output.puts "#{context} - Error: #{safe_message}"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Sanitize message to prevent breaking TTY display (remove newlines, truncate)
|
|
171
|
+
def sanitize_message(message)
|
|
172
|
+
return "" if message.nil?
|
|
173
|
+
|
|
174
|
+
# Replace newlines with spaces and collapse multiple spaces
|
|
175
|
+
clean = message.to_s.gsub(/[\r\n]+/, " ").gsub(/\s+/, " ").strip
|
|
176
|
+
# Truncate if too long
|
|
177
|
+
clean.length > 80 ? "#{clean[0, 77]}..." : clean
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Print a final summary (restores cursor and shows it)
|
|
181
|
+
# Note: Use finish_from_trap when called from a signal handler
|
|
182
|
+
def finish(message)
|
|
183
|
+
@mutex.synchronize do
|
|
184
|
+
finish_unsafe(message)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Trap-safe version of finish (no mutex, safe to call from signal handlers)
|
|
189
|
+
def finish_from_trap(message)
|
|
190
|
+
finish_unsafe(message)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Show interrupt message without clearing progress (called on first Ctrl-C)
|
|
194
|
+
# Note: Use interrupt_from_trap when called from a signal handler
|
|
195
|
+
def interrupt(message)
|
|
196
|
+
@mutex.synchronize do
|
|
197
|
+
interrupt_unsafe(message)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Trap-safe version of interrupt (no mutex, safe to call from signal handlers)
|
|
202
|
+
def interrupt_from_trap(message)
|
|
203
|
+
interrupt_unsafe(message)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
private
|
|
207
|
+
|
|
208
|
+
def finish_unsafe(message)
|
|
209
|
+
if @tty
|
|
210
|
+
# Restore cursor position and show cursor
|
|
211
|
+
@output.print CURSOR_RESTORE
|
|
212
|
+
# Clear from cursor to end of screen to remove progress lines
|
|
213
|
+
@output.print "\e[J"
|
|
214
|
+
@output.print CURSOR_SHOW
|
|
215
|
+
end
|
|
216
|
+
@output.puts message if message
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def interrupt_unsafe(message)
|
|
220
|
+
if @tty
|
|
221
|
+
# Move to the line below progress and print message
|
|
222
|
+
@output.print "\n"
|
|
223
|
+
@output.print "\e[2K"
|
|
224
|
+
@output.print "#{COLORS[:yellow]}#{message}#{COLORS[:reset]}"
|
|
225
|
+
@output.print "\n"
|
|
226
|
+
@output.flush
|
|
227
|
+
# Increase lines printed to account for the interrupt message
|
|
228
|
+
@lines_printed += 2
|
|
229
|
+
else
|
|
230
|
+
@output.puts message
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def build_overall_line
|
|
235
|
+
percentage = @total_imports.positive? ? ((@completed_imports.to_f / @total_imports) * 100).round : 0
|
|
236
|
+
progress_bar = build_progress_bar(percentage)
|
|
237
|
+
eta_str = calculate_eta_string
|
|
238
|
+
if @tty
|
|
239
|
+
"#{COLORS[:bold]}#{COLORS[:magenta]}Overall#{COLORS[:reset]} #{progress_bar} " \
|
|
240
|
+
"#{COLORS[:cyan]}#{percentage}%#{COLORS[:reset]} " \
|
|
241
|
+
"[#{@completed_imports}/#{@total_imports}] " \
|
|
242
|
+
"#{COLORS[:dim]}#{eta_str}#{COLORS[:reset]}"
|
|
243
|
+
else
|
|
244
|
+
"Overall: [#{@completed_imports}/#{@total_imports}] #{percentage}% #{eta_str}"
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def calculate_eta_string
|
|
249
|
+
return "ETA: --:--" if @start_time.nil? || @completed_imports.zero?
|
|
250
|
+
|
|
251
|
+
elapsed = Time.now - @start_time
|
|
252
|
+
remaining = @total_imports - @completed_imports
|
|
253
|
+
rate = @completed_imports.to_f / elapsed
|
|
254
|
+
eta_seconds = (remaining / rate).round
|
|
255
|
+
|
|
256
|
+
if eta_seconds < 60
|
|
257
|
+
"ETA: #{eta_seconds}s"
|
|
258
|
+
elsif eta_seconds < 3600
|
|
259
|
+
minutes = eta_seconds / 60
|
|
260
|
+
seconds = eta_seconds % 60
|
|
261
|
+
format("ETA: %d:%02d", minutes, seconds)
|
|
262
|
+
else
|
|
263
|
+
hours = eta_seconds / 3600
|
|
264
|
+
minutes = (eta_seconds % 3600) / 60
|
|
265
|
+
format("ETA: %d:%02d:%02d", hours, minutes, eta_seconds % 60)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def build_progress_bar(percentage)
|
|
270
|
+
width = 20
|
|
271
|
+
filled = [(percentage / 5.0).round, width].min
|
|
272
|
+
filled = [filled, 0].max
|
|
273
|
+
empty = width - filled
|
|
274
|
+
bar = "#{"█" * filled}#{"░" * empty}"
|
|
275
|
+
"#{COLORS[:green]}#{bar}#{COLORS[:reset]}"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def update_overall_line
|
|
279
|
+
# Move to first line (overall progress line), update, move back
|
|
280
|
+
lines_up = @lines_printed
|
|
281
|
+
@output.print "\e[#{lines_up}A"
|
|
282
|
+
@output.print "\e[2K"
|
|
283
|
+
@output.print build_overall_line
|
|
284
|
+
@output.print "\e[#{lines_up}B"
|
|
285
|
+
@output.print "\r"
|
|
286
|
+
@output.flush
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def build_line(context, message, current, total, color: nil)
|
|
290
|
+
parts = []
|
|
291
|
+
if @tty
|
|
292
|
+
parts << "#{COLORS[:bold]}#{context}#{COLORS[:reset]}"
|
|
293
|
+
parts << "#{COLORS[:cyan]}#{progress_indicator(current, total)}#{COLORS[:reset]}" if current && total
|
|
294
|
+
msg_color = color || COLORS[:reset]
|
|
295
|
+
parts << "#{msg_color}#{message}#{COLORS[:reset]}"
|
|
296
|
+
else
|
|
297
|
+
parts << context
|
|
298
|
+
parts << progress_indicator(current, total) if current && total
|
|
299
|
+
parts << message
|
|
300
|
+
end
|
|
301
|
+
parts.join(" - ")
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def progress_indicator(current, total)
|
|
305
|
+
percentage = ((current.to_f / total) * 100).round
|
|
306
|
+
"[#{current}/#{total} #{percentage}%]"
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Individual slot progress reporter
|
|
310
|
+
class SlotProgress
|
|
311
|
+
attr_reader :slot_id
|
|
312
|
+
attr_accessor :context
|
|
313
|
+
|
|
314
|
+
def initialize(parent, slot_id, context)
|
|
315
|
+
@parent = parent
|
|
316
|
+
@slot_id = slot_id
|
|
317
|
+
@context = context
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def update(message, current: nil, total: nil)
|
|
321
|
+
@parent.update_slot(@slot_id, @context, message, current: current, total: total)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def complete(message = nil)
|
|
325
|
+
@parent.complete_slot(@slot_id, @context, message)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def error(message)
|
|
329
|
+
@parent.error_slot(@slot_id, @context, message)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def warn(message)
|
|
333
|
+
# Warnings are shown inline with the current context (yellow in TTY mode)
|
|
334
|
+
@parent.update_slot(@slot_id, @context, "Warning: #{message}", color: COLORS[:yellow])
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def release
|
|
338
|
+
@parent.release_slot(@slot_id)
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|