odysseus-cli 0.1.0 → 0.3.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 +4 -4
- data/README.md +44 -0
- data/bin/odysseus +9 -1
- data/lib/odysseus/cli/cli.rb +308 -516
- data/lib/odysseus/cli/ui.rb +447 -0
- metadata +10 -9
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
# odysseus-cli/lib/odysseus/cli/ui.rb
|
|
2
|
+
#
|
|
3
|
+
# Terminal UI renderer for Odysseus CLI.
|
|
4
|
+
#
|
|
5
|
+
# Default mode: numbered steps with animated spinners that resolve to ✓/✗.
|
|
6
|
+
# Debug mode (--debug): verbose plain-text output, no spinners.
|
|
7
|
+
|
|
8
|
+
module Odysseus
|
|
9
|
+
module CLI
|
|
10
|
+
# IO wrapper that redacts sensitive values before writing
|
|
11
|
+
class RedactingIO
|
|
12
|
+
def initialize(io, redact_fn)
|
|
13
|
+
@io = io
|
|
14
|
+
@redact = redact_fn
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def write(str)
|
|
18
|
+
@io.write(@redact.call(str.to_s))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def puts(*args)
|
|
22
|
+
args.each { |a| @io.puts(@redact.call(a.to_s)) }
|
|
23
|
+
@io.puts if args.empty?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def print(*args)
|
|
27
|
+
args.each { |a| @io.print(@redact.call(a.to_s)) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def flush
|
|
31
|
+
@io.flush
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def respond_to_missing?(method, include_private = false)
|
|
35
|
+
@io.respond_to?(method, include_private)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def method_missing(method, *args, &block)
|
|
39
|
+
@io.send(method, *args, &block)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class UI
|
|
44
|
+
SPINNER_FRAMES = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].freeze
|
|
45
|
+
|
|
46
|
+
# ANSI color helpers
|
|
47
|
+
COPPER = "\e[38;2;255;183;123m".freeze
|
|
48
|
+
MINT = "\e[38;2;112;216;200m".freeze
|
|
49
|
+
RED = "\e[38;2;255;100;100m".freeze
|
|
50
|
+
DIM = "\e[2m".freeze
|
|
51
|
+
RESET = "\e[0m".freeze
|
|
52
|
+
|
|
53
|
+
def initialize(debug: false)
|
|
54
|
+
@debug = debug
|
|
55
|
+
@step_number = 0
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def debug?
|
|
59
|
+
@debug
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def reset_steps!
|
|
63
|
+
@step_number = 0
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def next_step!
|
|
67
|
+
@step_number += 1
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# --- Header ---
|
|
71
|
+
|
|
72
|
+
def header(title)
|
|
73
|
+
reset_steps!
|
|
74
|
+
if debug?
|
|
75
|
+
puts "\e[36m#{title}\e[0m"
|
|
76
|
+
else
|
|
77
|
+
puts ""
|
|
78
|
+
puts " #{COPPER}#{title}#{RESET}"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def info(label, value)
|
|
83
|
+
if debug?
|
|
84
|
+
puts " #{label}: #{value}"
|
|
85
|
+
else
|
|
86
|
+
puts " #{DIM}#{label}:#{RESET} #{value}"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def blank
|
|
91
|
+
puts ""
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# --- Single spin step ---
|
|
95
|
+
# Shows spinner while block runs, resolves to ✓/✗.
|
|
96
|
+
# Captures stdout from the block so it doesn't leak.
|
|
97
|
+
|
|
98
|
+
def spin_step(message)
|
|
99
|
+
next_step!
|
|
100
|
+
num = step_num_str
|
|
101
|
+
|
|
102
|
+
if debug?
|
|
103
|
+
puts " #{num} #{redact(message)}"
|
|
104
|
+
result = yield
|
|
105
|
+
puts " #{num} ✓ #{redact(message)}"
|
|
106
|
+
return result
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
result = nil
|
|
110
|
+
err = nil
|
|
111
|
+
done = false
|
|
112
|
+
|
|
113
|
+
# Capture stdout from the block
|
|
114
|
+
old_stdout = $stdout
|
|
115
|
+
rd, wr = IO.pipe
|
|
116
|
+
$stdout = wr
|
|
117
|
+
|
|
118
|
+
thread = Thread.new do
|
|
119
|
+
begin
|
|
120
|
+
result = yield
|
|
121
|
+
rescue => e
|
|
122
|
+
err = e
|
|
123
|
+
ensure
|
|
124
|
+
done = true
|
|
125
|
+
wr.close
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
frame_idx = 0
|
|
130
|
+
loop do
|
|
131
|
+
break if done
|
|
132
|
+
frame = SPINNER_FRAMES[frame_idx % SPINNER_FRAMES.size]
|
|
133
|
+
old_stdout.print "\r #{DIM}#{num}#{RESET} #{COPPER}#{frame}#{RESET} #{message}"
|
|
134
|
+
old_stdout.flush
|
|
135
|
+
frame_idx += 1
|
|
136
|
+
sleep 0.08
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
rd.close
|
|
140
|
+
$stdout = old_stdout
|
|
141
|
+
thread.join
|
|
142
|
+
|
|
143
|
+
print "\r\e[K"
|
|
144
|
+
|
|
145
|
+
if err
|
|
146
|
+
puts " #{DIM}#{num}#{RESET} #{RED}✗#{RESET} #{message}"
|
|
147
|
+
raise err
|
|
148
|
+
else
|
|
149
|
+
puts " #{DIM}#{num}#{RESET} #{MINT}✓#{RESET} #{message}"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
result
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# --- Streaming steps ---
|
|
156
|
+
# Runs a block, captures its stdout line by line, and renders each
|
|
157
|
+
# meaningful line as a sub-step with spinner → ✓.
|
|
158
|
+
# All sub-steps share the same step number.
|
|
159
|
+
|
|
160
|
+
def stream_steps(title: nil)
|
|
161
|
+
next_step!
|
|
162
|
+
num = step_num_str
|
|
163
|
+
|
|
164
|
+
if debug?
|
|
165
|
+
puts " #{num} > #{title}" if title
|
|
166
|
+
# In debug mode, let output flow but redact sensitive values
|
|
167
|
+
old_stdout = $stdout
|
|
168
|
+
$stdout = RedactingIO.new(old_stdout, method(:redact))
|
|
169
|
+
begin
|
|
170
|
+
yield
|
|
171
|
+
ensure
|
|
172
|
+
$stdout = old_stdout
|
|
173
|
+
end
|
|
174
|
+
return
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Show section header if provided
|
|
178
|
+
puts " #{DIM}#{num}#{RESET} #{COPPER}>#{RESET} #{title}" if title
|
|
179
|
+
|
|
180
|
+
result = nil
|
|
181
|
+
err = nil
|
|
182
|
+
done = false
|
|
183
|
+
current_line = nil
|
|
184
|
+
|
|
185
|
+
# Capture stdout
|
|
186
|
+
old_stdout = $stdout
|
|
187
|
+
rd, wr = IO.pipe
|
|
188
|
+
|
|
189
|
+
thread = Thread.new do
|
|
190
|
+
$stdout = wr
|
|
191
|
+
begin
|
|
192
|
+
result = yield
|
|
193
|
+
rescue => e
|
|
194
|
+
err = e
|
|
195
|
+
ensure
|
|
196
|
+
done = true
|
|
197
|
+
wr.close
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
frame_idx = 0
|
|
202
|
+
buf = ""
|
|
203
|
+
|
|
204
|
+
loop do
|
|
205
|
+
# Non-blocking read from pipe
|
|
206
|
+
begin
|
|
207
|
+
chunk = rd.read_nonblock(4096)
|
|
208
|
+
buf << chunk
|
|
209
|
+
rescue IO::WaitReadable
|
|
210
|
+
# No data available yet
|
|
211
|
+
rescue EOFError
|
|
212
|
+
break
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Process complete lines
|
|
216
|
+
while (nl = buf.index("\n"))
|
|
217
|
+
line = buf.slice!(0..nl).strip
|
|
218
|
+
next if line.empty?
|
|
219
|
+
line = clean_line(line)
|
|
220
|
+
next unless line
|
|
221
|
+
|
|
222
|
+
# Note lines (prefixed with ~) render as indented grey text, no spinner
|
|
223
|
+
if line.start_with?('~')
|
|
224
|
+
if current_line
|
|
225
|
+
old_stdout.print "\r\e[K"
|
|
226
|
+
old_stdout.puts " #{DIM}#{num}#{RESET} #{MINT}✓#{RESET} #{current_line}"
|
|
227
|
+
current_line = nil
|
|
228
|
+
end
|
|
229
|
+
old_stdout.puts " #{DIM}#{num}#{RESET} #{DIM}#{line[1..]}#{RESET}"
|
|
230
|
+
next
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Resolve previous sub-step
|
|
234
|
+
if current_line
|
|
235
|
+
old_stdout.print "\r\e[K"
|
|
236
|
+
old_stdout.puts " #{DIM}#{num}#{RESET} #{MINT}✓#{RESET} #{current_line}"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
current_line = line
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Animate spinner on current line
|
|
243
|
+
if current_line
|
|
244
|
+
frame = SPINNER_FRAMES[frame_idx % SPINNER_FRAMES.size]
|
|
245
|
+
old_stdout.print "\r #{DIM}#{num}#{RESET} #{COPPER}#{frame}#{RESET} #{current_line}"
|
|
246
|
+
old_stdout.flush
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
frame_idx += 1
|
|
250
|
+
sleep 0.06
|
|
251
|
+
|
|
252
|
+
break if done && buf.empty?
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
rd.close
|
|
256
|
+
$stdout = old_stdout
|
|
257
|
+
thread.join
|
|
258
|
+
|
|
259
|
+
# Resolve the final sub-step
|
|
260
|
+
if current_line
|
|
261
|
+
print "\r\e[K"
|
|
262
|
+
puts " #{DIM}#{num}#{RESET} #{MINT}✓#{RESET} #{current_line}"
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
raise err if err
|
|
266
|
+
result
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# --- Immediate steps (no async) ---
|
|
270
|
+
|
|
271
|
+
def step_ok(message)
|
|
272
|
+
next_step!
|
|
273
|
+
puts " #{DIM}#{step_num_str}#{RESET} #{MINT}✓#{RESET} #{message}"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def step_fail(message)
|
|
277
|
+
next_step!
|
|
278
|
+
puts " #{DIM}#{step_num_str}#{RESET} #{RED}✗#{RESET} #{message}"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def step_info(message)
|
|
282
|
+
next_step!
|
|
283
|
+
puts " #{DIM}#{step_num_str}#{RESET} #{COPPER}➜#{RESET} #{message}"
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# --- Simple output (no numbering) ---
|
|
287
|
+
|
|
288
|
+
def success(message)
|
|
289
|
+
puts " #{MINT}✓#{RESET} #{message}"
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def error(message)
|
|
293
|
+
puts " #{RED}✗#{RESET} #{message}"
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def warn(message)
|
|
297
|
+
puts " \e[33m!#{RESET} #{message}"
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def step(message)
|
|
301
|
+
if debug?
|
|
302
|
+
puts " #{redact(message)}"
|
|
303
|
+
else
|
|
304
|
+
puts " #{DIM}›#{RESET} #{message}"
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def detail(message)
|
|
309
|
+
puts " #{DIM}#{redact(message)}#{RESET}" if debug?
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# --- Tables ---
|
|
313
|
+
|
|
314
|
+
def table(headers:, rows:)
|
|
315
|
+
return if rows.empty?
|
|
316
|
+
|
|
317
|
+
widths = headers.map.with_index do |h, i|
|
|
318
|
+
[h.to_s.length, rows.map { |r| r[i].to_s.length }.max || 0].max
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
header_line = headers.map.with_index { |h, i| h.to_s.ljust(widths[i]) }.join(" ")
|
|
322
|
+
puts " #{COPPER}#{header_line}#{RESET}"
|
|
323
|
+
puts " #{widths.map { |w| '─' * w }.join(' ')}"
|
|
324
|
+
|
|
325
|
+
rows.each do |row|
|
|
326
|
+
line = row.map.with_index { |c, i| c.to_s.ljust(widths[i]) }.join(" ")
|
|
327
|
+
puts " #{line}"
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# --- Section divider ---
|
|
332
|
+
|
|
333
|
+
def section(title)
|
|
334
|
+
if debug?
|
|
335
|
+
puts "\e[36m=== #{title} ===\e[0m"
|
|
336
|
+
else
|
|
337
|
+
puts " #{COPPER}▸ #{title}#{RESET}"
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# --- Deploy-specific helpers ---
|
|
342
|
+
|
|
343
|
+
def deploy_header(service:, image:, image_tag:, build: false, distribution: nil)
|
|
344
|
+
header "Odysseus Deploy"
|
|
345
|
+
info "Service", service
|
|
346
|
+
info "Image", "#{image}:#{image_tag}"
|
|
347
|
+
info "Distribute", distribution if build && distribution
|
|
348
|
+
blank
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def deploy_complete(duration: nil)
|
|
352
|
+
msg = "Deployment successful"
|
|
353
|
+
msg += " in #{duration}s" if duration
|
|
354
|
+
step_ok msg
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# --- Logger adapter ---
|
|
358
|
+
|
|
359
|
+
def build_logger
|
|
360
|
+
ui = self
|
|
361
|
+
Object.new.tap do |l|
|
|
362
|
+
l.define_singleton_method(:info) { |msg| ui.step(msg) }
|
|
363
|
+
l.define_singleton_method(:warn) { |msg| ui.warn(msg) }
|
|
364
|
+
l.define_singleton_method(:error) { |msg| ui.error(msg) }
|
|
365
|
+
l.define_singleton_method(:debug) { |msg| ui.detail(msg) }
|
|
366
|
+
l.define_singleton_method(:verbose?) { ui.debug? }
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
private
|
|
371
|
+
|
|
372
|
+
def step_num_str
|
|
373
|
+
format('%02d', @step_number)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Redact sensitive values from output.
|
|
377
|
+
# Matches common patterns for API keys, tokens, passwords, and secrets
|
|
378
|
+
# passed as env vars or command flags.
|
|
379
|
+
def redact(text)
|
|
380
|
+
text
|
|
381
|
+
.gsub(/(-e\s+\w*(?:KEY|TOKEN|SECRET|PASSWORD|MASTER_KEY|API_KEY|CREDENTIALS)\s*=\s*)\S+/i, '\1[REDACTED]')
|
|
382
|
+
.gsub(/((?:KEY|TOKEN|SECRET|PASSWORD|MASTER_KEY|API_KEY|CREDENTIALS)\s*[=:]\s*)\S+/i, '\1[REDACTED]')
|
|
383
|
+
.gsub(/(-p\s+)\S+/, '\1[REDACTED]')
|
|
384
|
+
.gsub(/(--password\s+)\S+/, '\1[REDACTED]')
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# Clean up raw output lines from core orchestrators.
|
|
388
|
+
# Returns nil for lines we should skip.
|
|
389
|
+
def clean_line(line)
|
|
390
|
+
# Strip leading whitespace
|
|
391
|
+
line = line.sub(/^\s+/, '')
|
|
392
|
+
|
|
393
|
+
# Skip empty / decorative / noise
|
|
394
|
+
return nil if line.empty?
|
|
395
|
+
return nil if line.start_with?('===', '---', '[WARN]', '[ERROR]')
|
|
396
|
+
|
|
397
|
+
# Skip verbose detail lines
|
|
398
|
+
return nil if line.match?(/^Image: /)
|
|
399
|
+
return nil if line.match?(/^Environment: /)
|
|
400
|
+
return nil if line.match?(/^Resources: /)
|
|
401
|
+
return nil if line.match?(/^Volumes: /)
|
|
402
|
+
return nil if line.match?(/^Found \d+ existing container/)
|
|
403
|
+
return nil if line.match?(/^Deploying .+ \(role: .+\)/)
|
|
404
|
+
return nil if line.match?(/^Building locally/)
|
|
405
|
+
return nil if line.match?(/^Pushing image via SSH to/)
|
|
406
|
+
return nil if line.match?(/^Deploy complete for /)
|
|
407
|
+
return nil if line.match?(/^Rolling deploy complete/)
|
|
408
|
+
|
|
409
|
+
# Skip "done" echo lines — the spinner→✓ already shows completion
|
|
410
|
+
return nil if line.match?(/^Container started: /)
|
|
411
|
+
return nil if line.match?(/^Health check passed$/)
|
|
412
|
+
return nil if line.match?(/^Caddy routing configured$/)
|
|
413
|
+
return nil if line.match?(/^Old container removed$/)
|
|
414
|
+
return nil if line.match?(/^Image pulled$/)
|
|
415
|
+
return nil if line.match?(/^Attached to proxy$/)
|
|
416
|
+
|
|
417
|
+
# Note lines — indented grey text under the previous step
|
|
418
|
+
return '~Caddy already running' if line.match?(/^Caddy already running$/)
|
|
419
|
+
return '~Caddy started' if line.match?(/^Caddy started$/)
|
|
420
|
+
return '~Caddy is ready' if line.match?(/^Caddy is ready$/)
|
|
421
|
+
|
|
422
|
+
# Map known messages to clean versions
|
|
423
|
+
CLEAN_MESSAGES.each do |pattern, replacement|
|
|
424
|
+
if line.match?(pattern)
|
|
425
|
+
return line.sub(pattern, replacement)
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
line
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
CLEAN_MESSAGES = {
|
|
433
|
+
/^Ensuring Caddy proxy is running\.\.\./ => 'Starting Caddy',
|
|
434
|
+
/^Starting new container\.\.\.$/ => 'Starting container',
|
|
435
|
+
/^Waiting for health check.*/ => 'Health check',
|
|
436
|
+
/^Adding to Caddy proxy.*/ => 'Caddy route update',
|
|
437
|
+
/^Draining old container.*/ => 'Draining old container',
|
|
438
|
+
/^Pulling image\.\.\.$/ => 'Pulling image',
|
|
439
|
+
/^Building image: .+$/ => 'Building image',
|
|
440
|
+
/^Pushing to (.+)\.\.\./ => 'Pushing image to \1',
|
|
441
|
+
/^Starting (.+)\.\.\.$/ => 'Starting \1',
|
|
442
|
+
/^Stopping (.+) .*/ => 'Stopping \1',
|
|
443
|
+
/^Attaching (.+) to proxy.*/ => 'Attaching \1 to proxy',
|
|
444
|
+
}.freeze
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
end
|
metadata
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: odysseus-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
7
|
+
- Thomas
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
@@ -15,28 +15,28 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '0.
|
|
18
|
+
version: '0.2'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '0.
|
|
25
|
+
version: '0.2'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
|
-
name:
|
|
27
|
+
name: ratatui_ruby
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '
|
|
32
|
+
version: '1.4'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '
|
|
39
|
+
version: '1.4'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: rspec
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -67,7 +67,7 @@ dependencies:
|
|
|
67
67
|
version: '3.10'
|
|
68
68
|
description: Command-line interface for deploying with Odysseus
|
|
69
69
|
email:
|
|
70
|
-
-
|
|
70
|
+
- thomas@imfiny.com
|
|
71
71
|
executables:
|
|
72
72
|
- odysseus
|
|
73
73
|
extensions: []
|
|
@@ -76,7 +76,8 @@ files:
|
|
|
76
76
|
- README.md
|
|
77
77
|
- bin/odysseus
|
|
78
78
|
- lib/odysseus/cli/cli.rb
|
|
79
|
-
|
|
79
|
+
- lib/odysseus/cli/ui.rb
|
|
80
|
+
homepage: https://github.com/WA-Systems-EU/odysseus
|
|
80
81
|
licenses:
|
|
81
82
|
- LGPL-3.0-only
|
|
82
83
|
metadata: {}
|