omaship 0.4.0 → 0.6.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.
@@ -2,6 +2,8 @@ require "io/console"
2
2
 
3
3
  module Omaship
4
4
  class ColorPicker
5
+ include LinearPickerControls
6
+
5
7
  SCHEMES = [
6
8
  { key: "flexoki-light", bg: "#FFFCF0", primary: "#205EA6", accent: "#AD8301", text: "#100F0F" },
7
9
  { key: "flexoki-dark", bg: "#100F0F", primary: "#4385BE", accent: "#D0A215", text: "#FFFCF0" },
@@ -18,6 +20,8 @@ module Omaship
18
20
  ].freeze
19
21
 
20
22
  DEFAULT_INDEX = 9
23
+ SWATCH_SURFACE = "#FFFFFF"
24
+ SWATCH_BORDER = "#000000"
21
25
 
22
26
  def initialize(out: $stdout, input: $stdin)
23
27
  @out = out
@@ -28,24 +32,23 @@ module Omaship
28
32
  def pick
29
33
  render
30
34
  loop do
31
- key = read_key
35
+ key = read_linear_navigation_key(@input)
32
36
  case key
33
- when :left, "h"
37
+ when :previous
34
38
  @index = (@index - 1) % SCHEMES.size
35
39
  render
36
- when :right, "l"
40
+ when :next
37
41
  @index = (@index + 1) % SCHEMES.size
38
42
  render
39
43
  when :enter, "q"
40
- @out.print "\e[?25h"
41
- @out.puts
42
44
  return SCHEMES[@index][:key]
43
45
  when :ctrl_c
44
- @out.print "\e[?25h"
45
- @out.puts
46
- return SCHEMES[DEFAULT_INDEX][:key]
46
+ raise Interrupt
47
47
  end
48
48
  end
49
+ ensure
50
+ @out.print "\e[?25h"
51
+ @out.puts if @rendered_lines
49
52
  end
50
53
 
51
54
  private
@@ -65,26 +68,18 @@ module Omaship
65
68
 
66
69
  def build_preview(scheme)
67
70
  name = scheme[:key]
68
- bg_block = color_block(scheme[:bg])
69
- primary_block = color_block(scheme[:primary])
70
- accent_block = color_block(scheme[:accent])
71
- text_block = color_block(scheme[:text])
72
-
73
- nav = SCHEMES.map.with_index do |s, i|
74
- if i == @index
75
- "\e[1m[#{s[:key]}]\e[0m"
76
- else
77
- " #{s[:key]} "
78
- end
79
- end
71
+ bg_block = color_swatch(scheme[:bg], "bg")
72
+ primary_block = color_swatch(scheme[:primary], "primary")
73
+ accent_block = color_swatch(scheme[:accent], "accent")
74
+ text_block = color_swatch(scheme[:text], "text")
80
75
 
81
76
  [
82
77
  "",
83
78
  " #{left_arrow} #{name} #{right_arrow}",
84
79
  "",
85
- " #{bg_block} bg #{primary_block} primary #{accent_block} accent #{text_block} text",
80
+ " #{bg_block} #{primary_block} #{accent_block} #{text_block}",
86
81
  "",
87
- " Use \e[1m<-\e[0m / \e[1m->\e[0m or \e[1mh\e[0m / \e[1ml\e[0m to browse. \e[1mEnter\e[0m to select.",
82
+ linear_navigation_hint,
88
83
  ""
89
84
  ]
90
85
  end
@@ -97,33 +92,37 @@ module Omaship
97
92
  @index < SCHEMES.size - 1 ? "\e[1m>\e[0m" : " "
98
93
  end
99
94
 
95
+ def color_swatch(hex, label)
96
+ "#{color_block(hex)} #{label}"
97
+ end
98
+
100
99
  def color_block(hex)
100
+ [
101
+ background_escape(SWATCH_SURFACE),
102
+ foreground_escape(SWATCH_BORDER),
103
+ "[",
104
+ background_escape(hex),
105
+ " ",
106
+ background_escape(SWATCH_SURFACE),
107
+ foreground_escape(SWATCH_BORDER),
108
+ "]",
109
+ "\e[0m"
110
+ ].join
111
+ end
112
+
113
+ def background_escape(hex)
101
114
  r, g, b = hex_to_rgb(hex)
102
- "\e[48;2;#{r};#{g};#{b}m \e[0m"
115
+ "\e[48;2;#{r};#{g};#{b}m"
116
+ end
117
+
118
+ def foreground_escape(hex)
119
+ r, g, b = hex_to_rgb(hex)
120
+ "\e[38;2;#{r};#{g};#{b}m"
103
121
  end
104
122
 
105
123
  def hex_to_rgb(hex)
106
124
  hex = hex.delete("#")
107
125
  [ hex[0..1], hex[2..3], hex[4..5] ].map { |c| c.to_i(16) }
108
126
  end
109
-
110
- def read_key
111
- char = @input.raw { @input.getc }
112
- case char
113
- when "\r", "\n"
114
- :enter
115
- when "\e"
116
- seq = @input.raw { @input.read_nonblock(2) rescue "" }
117
- case seq
118
- when "[D" then :left
119
- when "[C" then :right
120
- else :unknown
121
- end
122
- when "\x03"
123
- :ctrl_c
124
- else
125
- char
126
- end
127
- end
128
127
  end
129
128
  end
@@ -0,0 +1,72 @@
1
+ require "io/console"
2
+
3
+ module Omaship
4
+ class HarborPicker
5
+ include LinearPickerControls
6
+
7
+ def initialize(harbors:, out: $stdout, input: $stdin)
8
+ @harbors = Array(harbors)
9
+ @out = out
10
+ @input = input
11
+ @index = 0
12
+ end
13
+
14
+ def pick
15
+ render
16
+ loop do
17
+ key = read_linear_navigation_key(@input)
18
+
19
+ case key
20
+ when :previous
21
+ @index = (@index - 1) % @harbors.size
22
+ render
23
+ when :next
24
+ @index = (@index + 1) % @harbors.size
25
+ render
26
+ when :enter, "q"
27
+ return @harbors.fetch(@index).fetch("id")
28
+ when :ctrl_c
29
+ raise Interrupt
30
+ end
31
+ end
32
+ ensure
33
+ @out.print "\e[?25h"
34
+ @out.puts if @rendered_lines
35
+ end
36
+
37
+ private
38
+ def render
39
+ @out.print "\e[?25l"
40
+ @out.print "\r\e[K"
41
+ lines = build_preview
42
+ move_up = @rendered_lines || 0
43
+ @out.print "\e[#{move_up}A" if move_up > 0
44
+ lines.each do |line|
45
+ @out.print "\r\e[K#{line}\n"
46
+ end
47
+ @rendered_lines = lines.size
48
+ end
49
+
50
+ def build_preview
51
+ [
52
+ "",
53
+ *harbor_rows,
54
+ "",
55
+ linear_navigation_hint,
56
+ ""
57
+ ]
58
+ end
59
+
60
+ def harbor_rows
61
+ width = @harbors.map { |harbor| harbor.fetch("name").length }.max || 0
62
+
63
+ @harbors.map.with_index do |harbor, index|
64
+ marker = index == @index ? ">" : " "
65
+ label = harbor.fetch("name").ljust(width)
66
+ summary = harbor.fetch("management", "managed").capitalize
67
+ line = "#{marker} #{label} #{summary}"
68
+ " #{index == @index ? "\e[1m#{line}\e[0m" : line}"
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,34 @@
1
+ module Omaship
2
+ module LinearPickerControls
3
+ private
4
+ def linear_navigation_hint
5
+ " Navigate with arrows or h/j/k/l. Enter to select."
6
+ end
7
+
8
+ def read_linear_navigation_key(input)
9
+ char = input.raw { input.getc }
10
+ case char
11
+ when "\r", "\n"
12
+ :enter
13
+ when "\e"
14
+ seq = input.raw { input.read_nonblock(2) rescue "" }
15
+ case seq
16
+ when "[A", "[D"
17
+ :previous
18
+ when "[B", "[C"
19
+ :next
20
+ else
21
+ :unknown
22
+ end
23
+ when "\x03"
24
+ :ctrl_c
25
+ when "h", "k"
26
+ :previous
27
+ when "j", "l"
28
+ :next
29
+ else
30
+ char
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,8 +1,22 @@
1
1
  module Omaship
2
2
  class ProgressRenderer
3
+ SPINNER_FRAMES = [
4
+ "[= ]",
5
+ "[ = ]",
6
+ "[ = ]",
7
+ "[ =]",
8
+ "[ = ]",
9
+ "[ = ]"
10
+ ].freeze
11
+
3
12
  def initialize(out:)
4
13
  @out = out
5
14
  @seen = {}
15
+ @activity_mutex = Mutex.new
16
+ @activity_message = nil
17
+ @activity_eta = nil
18
+ @current_frame = SPINNER_FRAMES.first
19
+ @cursor_hidden = false
6
20
  end
7
21
 
8
22
  def step(message)
@@ -11,5 +25,138 @@ module Omaship
11
25
  @seen[message] = true
12
26
  @out.puts message
13
27
  end
28
+
29
+ def start_activity(message, eta: nil)
30
+ @activity_mutex.synchronize do
31
+ @activity_message = message
32
+ @activity_eta = eta
33
+ end
34
+
35
+ if interactive_output?
36
+ hide_cursor
37
+ render_activity_frame(@current_frame)
38
+ start_spinner
39
+ else
40
+ step(activity_line(message:, eta:))
41
+ end
42
+ end
43
+
44
+ def update_activity(message: nil, eta: nil)
45
+ @activity_mutex.synchronize do
46
+ @activity_message = message if message
47
+ @activity_eta = eta if eta
48
+ end
49
+
50
+ if interactive_output?
51
+ render_activity_frame(@current_frame)
52
+ else
53
+ line = nil
54
+ @activity_mutex.synchronize do
55
+ line = activity_line(message: @activity_message, eta: @activity_eta)
56
+ end
57
+ step(line)
58
+ end
59
+ end
60
+
61
+ def finish_activity(message = nil)
62
+ stop_spinner
63
+ step(message) if message
64
+ end
65
+
66
+ private
67
+ def interactive_output?
68
+ @out.respond_to?(:tty?) && @out.tty?
69
+ end
70
+
71
+ def start_spinner
72
+ return if @spinner_thread&.alive?
73
+
74
+ @spinner_stop = false
75
+ @spinner_thread = Thread.new do
76
+ index = 1
77
+
78
+ until @spinner_stop
79
+ frame = SPINNER_FRAMES[index % SPINNER_FRAMES.length]
80
+ render_activity_frame(frame)
81
+ sleep 0.12
82
+ index += 1
83
+ end
84
+ end
85
+ end
86
+
87
+ def stop_spinner
88
+ return unless @spinner_thread
89
+
90
+ @spinner_stop = true
91
+ @spinner_thread.join(0.5)
92
+ @spinner_thread.kill if @spinner_thread.alive?
93
+ @spinner_thread = nil
94
+ clear_line
95
+ show_cursor
96
+ end
97
+
98
+ def activity_line(message:, eta:)
99
+ normalized_message = message.to_s.strip
100
+ normalized_eta = eta.to_s.strip
101
+
102
+ if normalized_eta.empty?
103
+ normalized_message
104
+ else
105
+ "#{normalized_message} (ETA #{normalized_eta})"
106
+ end
107
+ end
108
+
109
+ def render_activity_frame(frame)
110
+ line = nil
111
+
112
+ @activity_mutex.synchronize do
113
+ @current_frame = frame
114
+ line = "#{frame} #{activity_line(message: @activity_message, eta: @activity_eta)}"
115
+ end
116
+
117
+ clear_line
118
+ @out.print(truncate_for_terminal(line))
119
+ @out.flush if @out.respond_to?(:flush)
120
+ end
121
+
122
+ def clear_line
123
+ @out.print("\r\e[2K")
124
+ end
125
+
126
+ def hide_cursor
127
+ return if @cursor_hidden
128
+
129
+ @out.print("\e[?25l")
130
+ @out.flush if @out.respond_to?(:flush)
131
+ @cursor_hidden = true
132
+ end
133
+
134
+ def show_cursor
135
+ return unless @cursor_hidden
136
+
137
+ @out.print("\e[?25h")
138
+ @out.flush if @out.respond_to?(:flush)
139
+ @cursor_hidden = false
140
+ end
141
+
142
+ def truncate_for_terminal(line)
143
+ maximum_width = [ terminal_width - 1, 1 ].max
144
+ return line if line.length <= maximum_width
145
+ return line[0, maximum_width] if maximum_width <= 3
146
+
147
+ "#{line[0, maximum_width - 3]}..."
148
+ end
149
+
150
+ def terminal_width
151
+ width = if @out.respond_to?(:winsize)
152
+ Array(@out.winsize)[1]
153
+ elsif IO.respond_to?(:console) && IO.console
154
+ IO.console.winsize[1]
155
+ end
156
+
157
+ width.to_i > 0 ? width.to_i : 80
158
+ rescue IOError, NoMethodError
159
+ 80
160
+ end
14
161
  end
15
162
  end
@@ -3,7 +3,7 @@ require "open3"
3
3
  module Omaship
4
4
  class ShipDetector
5
5
  class DetectionError < StandardError; end
6
- SHIP_CONTEXT_ERROR_MESSAGE = "Could not determine which ship to use automatically. Pass --ship <org/repo> (or ship id) to choose a ship.".freeze
6
+ SHIP_CONTEXT_ERROR_MESSAGE = "Could not determine which ship to use automatically. Pass --ship <ship-domain> (or ship id) to choose a ship.".freeze
7
7
 
8
8
  def initialize(api_client:)
9
9
  @api_client = api_client
@@ -0,0 +1,98 @@
1
+ require "io/console"
2
+
3
+ module Omaship
4
+ class ShipTypePicker
5
+ include LinearPickerControls
6
+
7
+ OPTIONS = [
8
+ {
9
+ key: "landing",
10
+ label: "Landingpage",
11
+ summary: "Public landing page with waitlist capture"
12
+ },
13
+ {
14
+ key: "app",
15
+ label: "Full App",
16
+ summary: "Authenticated product UI with dashboard"
17
+ },
18
+ {
19
+ key: "api",
20
+ label: "Agent API",
21
+ summary: "Agent-first API with CLI scaffolding"
22
+ }
23
+ ].freeze
24
+
25
+ DEFAULT_INDEX = 1
26
+
27
+ def self.label_for(key)
28
+ option = OPTIONS.find { |entry| entry[:key] == key }
29
+ option ? option[:label] : key.to_s
30
+ end
31
+
32
+ def initialize(out: $stdout, input: $stdin)
33
+ @out = out
34
+ @input = input
35
+ @index = DEFAULT_INDEX
36
+ end
37
+
38
+ def pick
39
+ render
40
+ loop do
41
+ key = read_linear_navigation_key(@input)
42
+
43
+ case key
44
+ when :previous
45
+ @index = (@index - 1) % OPTIONS.size
46
+ render
47
+ when :next
48
+ @index = (@index + 1) % OPTIONS.size
49
+ render
50
+ when :enter, "q"
51
+ return OPTIONS[@index][:key]
52
+ when :ctrl_c
53
+ raise Interrupt
54
+ end
55
+ end
56
+ ensure
57
+ @out.print "\e[?25h"
58
+ @out.puts if @rendered_lines
59
+ end
60
+
61
+ private
62
+
63
+ def render
64
+ option = OPTIONS[@index]
65
+ @out.print "\e[?25l"
66
+ @out.print "\r\e[K"
67
+ lines = build_preview(option)
68
+ move_up = @rendered_lines || 0
69
+ @out.print "\e[#{move_up}A" if move_up > 0
70
+ lines.each do |line|
71
+ @out.print "\r\e[K#{line}\n"
72
+ end
73
+ @rendered_lines = lines.size
74
+ end
75
+
76
+ def build_preview(option)
77
+ [
78
+ "",
79
+ *option_rows(option: option),
80
+ "",
81
+ linear_navigation_hint,
82
+ ""
83
+ ]
84
+ end
85
+
86
+ def option_rows(option:)
87
+ OPTIONS.map do |entry|
88
+ marker = entry == option ? ">" : " "
89
+ line = "#{marker} #{entry[:label].ljust(label_width)} #{entry[:summary]}"
90
+ " #{entry == option ? "\e[1m#{line}\e[0m" : line}"
91
+ end
92
+ end
93
+
94
+ def label_width
95
+ @label_width ||= OPTIONS.map { |entry| entry[:label].length }.max
96
+ end
97
+ end
98
+ end
@@ -1,3 +1,3 @@
1
1
  module Omaship
2
- VERSION = "0.4.0".freeze
2
+ VERSION = "0.6.0".freeze
3
3
  end
data/lib/omaship.rb CHANGED
@@ -6,5 +6,8 @@ require_relative "omaship/api_client"
6
6
  require_relative "omaship/credentials"
7
7
  require_relative "omaship/progress_renderer"
8
8
  require_relative "omaship/ship_detector"
9
+ require_relative "omaship/linear_picker_controls"
9
10
  require_relative "omaship/color_picker"
11
+ require_relative "omaship/harbor_picker"
12
+ require_relative "omaship/ship_type_picker"
10
13
  require_relative "omaship/cli"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omaship
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Omaship
@@ -51,7 +51,7 @@ dependencies:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0'
54
- description: CLI for login, ship provisioning, and deploy operations against Omaship.
54
+ description: CLI for login, ship provisioning, and ship management against Omaship.
55
55
  email:
56
56
  - hello@omaship.com
57
57
  executables:
@@ -67,8 +67,11 @@ files:
67
67
  - lib/omaship/cli.rb
68
68
  - lib/omaship/color_picker.rb
69
69
  - lib/omaship/credentials.rb
70
+ - lib/omaship/harbor_picker.rb
71
+ - lib/omaship/linear_picker_controls.rb
70
72
  - lib/omaship/progress_renderer.rb
71
73
  - lib/omaship/ship_detector.rb
74
+ - lib/omaship/ship_type_picker.rb
72
75
  - lib/omaship/version.rb
73
76
  homepage: https://omaship.com
74
77
  licenses:
@@ -91,7 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
91
94
  - !ruby/object:Gem::Version
92
95
  version: '0'
93
96
  requirements: []
94
- rubygems_version: 4.0.3
97
+ rubygems_version: 4.0.6
95
98
  specification_version: 4
96
99
  summary: Omaship command-line interface
97
100
  test_files: []