omaship 0.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +5 -4
- data/lib/omaship/api_client.rb +22 -8
- data/lib/omaship/cli.rb +371 -147
- data/lib/omaship/color_picker.rb +41 -42
- data/lib/omaship/harbor_picker.rb +72 -0
- data/lib/omaship/linear_picker_controls.rb +34 -0
- data/lib/omaship/progress_renderer.rb +147 -0
- data/lib/omaship/ship_detector.rb +1 -1
- data/lib/omaship/ship_type_picker.rb +98 -0
- data/lib/omaship/version.rb +1 -1
- data/lib/omaship.rb +3 -0
- metadata +5 -2
data/lib/omaship/cli.rb
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
1
|
require "thor"
|
|
2
|
+
require "securerandom"
|
|
3
|
+
require "uri"
|
|
2
4
|
|
|
3
5
|
module Omaship
|
|
4
6
|
class CLI < Thor
|
|
7
|
+
PROVISION_STEP_MESSAGES = {
|
|
8
|
+
"resolve_source" => "Preparing your ship run",
|
|
9
|
+
"prepare_server" => "Preparing your harbor",
|
|
10
|
+
"scaffold_app" => "Scaffolding your ship",
|
|
11
|
+
"deploy_app" => "Deploying your ship",
|
|
12
|
+
"create_tunnel" => "Configuring private access",
|
|
13
|
+
"cutover_dns" => "Wiring up your ship routes",
|
|
14
|
+
"lockdown_firewall" => "Locking down the harbor",
|
|
15
|
+
"healthcheck" => "Running final health checks"
|
|
16
|
+
}.freeze
|
|
17
|
+
PROVISION_STEP_ETAS = {
|
|
18
|
+
"resolve_source" => "about 2 min",
|
|
19
|
+
"prepare_server" => "about 2 min",
|
|
20
|
+
"scaffold_app" => "about 90 sec",
|
|
21
|
+
"deploy_app" => "about 90 sec",
|
|
22
|
+
"create_tunnel" => "about 45 sec",
|
|
23
|
+
"cutover_dns" => "about 30 sec",
|
|
24
|
+
"lockdown_firewall" => "about 20 sec",
|
|
25
|
+
"healthcheck" => "less than 15 sec"
|
|
26
|
+
}.freeze
|
|
27
|
+
DEFAULT_PROVISIONING_ETA = "about 2 min"
|
|
28
|
+
|
|
5
29
|
map "new" => :new_ship
|
|
6
30
|
map "status" => :info
|
|
7
31
|
map "ship" => :info
|
|
@@ -13,8 +37,16 @@ module Omaship
|
|
|
13
37
|
end
|
|
14
38
|
|
|
15
39
|
def self.start(given_args = ARGV, config = {})
|
|
40
|
+
if root_version_requested?(given_args)
|
|
41
|
+
$stdout.puts Omaship::VERSION
|
|
42
|
+
return
|
|
43
|
+
end
|
|
44
|
+
|
|
16
45
|
normalized_args = normalize_help_args(given_args)
|
|
17
46
|
super(normalized_args, config)
|
|
47
|
+
rescue Interrupt
|
|
48
|
+
$stderr.puts "\nAborted."
|
|
49
|
+
exit(130)
|
|
18
50
|
end
|
|
19
51
|
|
|
20
52
|
def self.normalize_help_args(given_args)
|
|
@@ -36,6 +68,10 @@ module Omaship
|
|
|
36
68
|
args == [ "-h" ] || args == [ "--help" ] || args == [ "help" ]
|
|
37
69
|
end
|
|
38
70
|
|
|
71
|
+
def self.root_version_requested?(args)
|
|
72
|
+
args == [ "-v" ] || args == [ "--version" ]
|
|
73
|
+
end
|
|
74
|
+
|
|
39
75
|
def self.help(shell, subcommand = false)
|
|
40
76
|
if subcommand
|
|
41
77
|
super
|
|
@@ -48,10 +84,9 @@ module Omaship
|
|
|
48
84
|
shell.say " login Store API token credentials locally"
|
|
49
85
|
shell.say " whoami Show authenticated user"
|
|
50
86
|
shell.say " list List ships you can access"
|
|
51
|
-
shell.say " use Set default ship from `omaship list` (for example: `omaship use omaship
|
|
87
|
+
shell.say " use Set default ship from `omaship list` (for example: `omaship use acme.omaship.app`)"
|
|
52
88
|
shell.say " info Show details for a ship (aliases: ship, status)"
|
|
53
89
|
shell.say " new Create and provision a new ship"
|
|
54
|
-
shell.say " deploy Deploy a ship (requires Full CLI access)"
|
|
55
90
|
shell.say " upgrade Open browser to upgrade your plan"
|
|
56
91
|
shell.say " complete Print shell completion script (bash, zsh, fish)"
|
|
57
92
|
shell.say " logout Remove local CLI credentials"
|
|
@@ -101,26 +136,14 @@ module Omaship
|
|
|
101
136
|
auth_payload = client.authenticate
|
|
102
137
|
user_payload = auth_payload.fetch("user")
|
|
103
138
|
account_payload = auth_payload.fetch("account", {})
|
|
104
|
-
token_payload = auth_payload.fetch("token", {})
|
|
105
139
|
ship_count = account_payload.fetch("ship_count", 0).to_i
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if access_level.empty?
|
|
109
|
-
access_level = "unknown"
|
|
110
|
-
end
|
|
111
|
-
default_ship_reference = credentials.default_ship.to_s.strip
|
|
140
|
+
plan = normalized_plan_label(plan: user_payload["plan"], admin: user_payload["admin"])
|
|
141
|
+
default_ship_reference = current_default_ship_reference(token: token)
|
|
112
142
|
|
|
113
143
|
say "User: #{user_payload.fetch("email_address")}"
|
|
114
144
|
say "Host: #{resolved_host}"
|
|
115
|
-
say "
|
|
145
|
+
say "Plan: #{plan}"
|
|
116
146
|
say "Ships: #{ship_count}"
|
|
117
|
-
if ship_count.zero?
|
|
118
|
-
say "Orgs: none (no ships yet)"
|
|
119
|
-
elsif orgs.empty?
|
|
120
|
-
say "Orgs: unavailable"
|
|
121
|
-
else
|
|
122
|
-
say "Orgs: #{orgs.join(", ")}"
|
|
123
|
-
end
|
|
124
147
|
|
|
125
148
|
if default_ship_reference.empty?
|
|
126
149
|
say "Default Ship: -"
|
|
@@ -135,7 +158,7 @@ module Omaship
|
|
|
135
158
|
with_api_error_handling(command: :list) do
|
|
136
159
|
token = current_token
|
|
137
160
|
ships = build_api_client(token: token).list_ships
|
|
138
|
-
default_ship_reference =
|
|
161
|
+
default_ship_reference = current_default_ship_reference(token: token, ships: ships)
|
|
139
162
|
|
|
140
163
|
if ships.empty?
|
|
141
164
|
say "No ships available yet."
|
|
@@ -143,7 +166,7 @@ module Omaship
|
|
|
143
166
|
say "Ships:"
|
|
144
167
|
ships.each do |ship|
|
|
145
168
|
marker = default_ship_match?(ship: ship, ship_reference: default_ship_reference) ? "*" : " "
|
|
146
|
-
say
|
|
169
|
+
say ship_list_line(ship:, marker:)
|
|
147
170
|
end
|
|
148
171
|
|
|
149
172
|
if !default_ship_reference.empty?
|
|
@@ -159,12 +182,11 @@ module Omaship
|
|
|
159
182
|
Set the default ship used by commands when `--ship` is omitted.
|
|
160
183
|
|
|
161
184
|
Get available values with `omaship list`.
|
|
162
|
-
|
|
163
|
-
Landing pages can also be selected by root domain, for example `acme.omaship.app`.
|
|
185
|
+
Use the ship domain from `omaship list`, for example `acme.omaship.app`.
|
|
164
186
|
Numeric ship ids from `omaship list` also work.
|
|
165
187
|
|
|
166
188
|
Examples:
|
|
167
|
-
omaship use omaship
|
|
189
|
+
omaship use acme.omaship.app
|
|
168
190
|
omaship use 17
|
|
169
191
|
DESC
|
|
170
192
|
def use(ship_reference)
|
|
@@ -178,7 +200,7 @@ module Omaship
|
|
|
178
200
|
end
|
|
179
201
|
|
|
180
202
|
desc "info", "Show ship info"
|
|
181
|
-
method_option :ship, type: :string, desc: "Ship from `omaship list` (
|
|
203
|
+
method_option :ship, type: :string, desc: "Ship from `omaship list` (use the ship domain or id)"
|
|
182
204
|
def info
|
|
183
205
|
with_api_error_handling(command: :info) do
|
|
184
206
|
token = current_token
|
|
@@ -186,59 +208,54 @@ module Omaship
|
|
|
186
208
|
resolved_ship = resolve_ship(token: token)
|
|
187
209
|
ship = client.ship(ship_id: resolved_ship.fetch("id")).fetch("ship")
|
|
188
210
|
default_ship_reference = credentials.default_ship.to_s.strip
|
|
211
|
+
latest_voyage = latest_voyage_summary_for(ship:)
|
|
189
212
|
|
|
190
213
|
say "Ship: #{ship.fetch("display_name")}"
|
|
191
214
|
say "ID: #{ship.fetch("id")}"
|
|
192
215
|
say "Status: #{ship.fetch("status")}"
|
|
193
|
-
say "
|
|
194
|
-
|
|
195
|
-
|
|
216
|
+
say "Domain: #{ship["root_domain"] || "-"}"
|
|
217
|
+
app_url = ship["app_url"].to_s.strip
|
|
218
|
+
if !app_url.empty?
|
|
219
|
+
say "App: #{app_url}"
|
|
220
|
+
end
|
|
221
|
+
api_url = ship["api_url"].to_s.strip
|
|
222
|
+
if !api_url.empty?
|
|
223
|
+
say "API: #{api_url}"
|
|
224
|
+
end
|
|
196
225
|
say "Default Ship: #{default_ship_reference.empty? ? "-" : default_ship_reference}"
|
|
197
226
|
|
|
198
|
-
if
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if deploy["run_number"]
|
|
203
|
-
say "Run: ##{deploy.fetch("run_number")}"
|
|
204
|
-
end
|
|
205
|
-
if deploy["started_at"]
|
|
206
|
-
say "Started At: #{deploy.fetch("started_at")}"
|
|
227
|
+
if latest_voyage
|
|
228
|
+
say "Latest Voyage: #{latest_voyage.fetch(:kind)} (#{latest_voyage.fetch(:status)})"
|
|
229
|
+
if latest_voyage[:id]
|
|
230
|
+
say "Voyage ID: ##{latest_voyage.fetch(:id)}"
|
|
207
231
|
end
|
|
208
232
|
end
|
|
209
233
|
end
|
|
210
234
|
end
|
|
211
235
|
|
|
212
236
|
desc "new NAME", "Create and provision a new ship"
|
|
213
|
-
method_option :domain, type: :string, desc: "
|
|
237
|
+
method_option :domain, type: :string, desc: "Custom root domain"
|
|
214
238
|
def new_ship(name)
|
|
215
239
|
with_api_error_handling(command: :new_ship) do
|
|
216
240
|
token = current_token
|
|
217
241
|
auth = fetch_auth(token: token)
|
|
218
|
-
|
|
242
|
+
allowed_starter_kinds = allowed_starter_kinds_from_auth(auth)
|
|
219
243
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
else
|
|
223
|
-
create_paid_ship(name: name, token: token)
|
|
244
|
+
unless can_create_ship_from_auth?(auth)
|
|
245
|
+
raise Thor::Error, "Ship limit reached for your plan. Run `omaship upgrade` to create another ship."
|
|
224
246
|
end
|
|
225
|
-
end
|
|
226
|
-
end
|
|
227
247
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
token = current_token
|
|
233
|
-
ship = resolve_ship_for_deploy(token: token)
|
|
234
|
-
ship_name = ship.fetch("full_name", "##{ship.fetch("id")}")
|
|
235
|
-
|
|
236
|
-
progress_renderer.step("Target ship: #{ship_name}.")
|
|
237
|
-
|
|
238
|
-
build_api_client(token: token).create_deploy(ship_id: ship.fetch("id"))
|
|
239
|
-
progress_renderer.step("Deploy requested.")
|
|
248
|
+
if allowed_starter_kinds == [ "landing" ]
|
|
249
|
+
create_free_ship(name: name, token: token)
|
|
250
|
+
else
|
|
251
|
+
starter_kind = choose_paid_starter_kind
|
|
240
252
|
|
|
241
|
-
|
|
253
|
+
if starter_kind == "landing"
|
|
254
|
+
create_paid_landingpage(name: name, token: token)
|
|
255
|
+
else
|
|
256
|
+
create_paid_ship(name: name, token: token, starter_kind: starter_kind, auth: auth)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
242
259
|
end
|
|
243
260
|
end
|
|
244
261
|
|
|
@@ -300,7 +317,7 @@ module Omaship
|
|
|
300
317
|
fi
|
|
301
318
|
|
|
302
319
|
if [[ ${COMP_CWORD} -eq 1 ]]; then
|
|
303
|
-
COMPREPLY=( $(compgen -W "login whoami list use info status ship new
|
|
320
|
+
COMPREPLY=( $(compgen -W "login whoami list use info status ship new upgrade logout complete help" -- "${cur}") )
|
|
304
321
|
return 0
|
|
305
322
|
fi
|
|
306
323
|
|
|
@@ -329,7 +346,7 @@ module Omaship
|
|
|
329
346
|
use)
|
|
330
347
|
COMPREPLY=( $(compgen -W "--host -h --help" -- "${cur}") )
|
|
331
348
|
;;
|
|
332
|
-
info|status|ship
|
|
349
|
+
info|status|ship)
|
|
333
350
|
COMPREPLY=( $(compgen -W "--ship --host -h --help" -- "${cur}") )
|
|
334
351
|
;;
|
|
335
352
|
new)
|
|
@@ -371,7 +388,6 @@ module Omaship
|
|
|
371
388
|
'status:Alias for info'
|
|
372
389
|
'ship:Alias for info'
|
|
373
390
|
'new:Create and provision a new ship'
|
|
374
|
-
'deploy:Deploy a ship'
|
|
375
391
|
'upgrade:Open browser to upgrade your plan'
|
|
376
392
|
'logout:Remove local CLI credentials'
|
|
377
393
|
'complete:Print shell completion script'
|
|
@@ -398,7 +414,7 @@ module Omaship
|
|
|
398
414
|
use)
|
|
399
415
|
_arguments '--host[API host]:host:' '1:ship reference:_omaship_ship_refs'
|
|
400
416
|
;;
|
|
401
|
-
info|status|ship
|
|
417
|
+
info|status|ship)
|
|
402
418
|
_arguments '--ship[Ship from omaship list]:ship:_omaship_ship_refs' '--host[API host]:host:'
|
|
403
419
|
;;
|
|
404
420
|
new)
|
|
@@ -426,12 +442,23 @@ module Omaship
|
|
|
426
442
|
end
|
|
427
443
|
|
|
428
444
|
complete -c omaship -f
|
|
429
|
-
complete -c omaship -n '__fish_use_subcommand' -a 'login whoami list use info status ship new deploy upgrade logout complete help'
|
|
430
445
|
complete -c omaship -l host -d 'API host'
|
|
446
|
+
complete -c omaship -n '__fish_use_subcommand' -a 'login' -d 'Store API token credentials locally'
|
|
447
|
+
complete -c omaship -n '__fish_use_subcommand' -a 'whoami' -d 'Show authenticated user'
|
|
448
|
+
complete -c omaship -n '__fish_use_subcommand' -a 'list' -d 'List ships you can access'
|
|
449
|
+
complete -c omaship -n '__fish_use_subcommand' -a 'use' -d 'Set default ship from omaship list'
|
|
450
|
+
complete -c omaship -n '__fish_use_subcommand' -a 'info' -d 'Show ship info'
|
|
451
|
+
complete -c omaship -n '__fish_use_subcommand' -a 'status' -d 'Alias for info'
|
|
452
|
+
complete -c omaship -n '__fish_use_subcommand' -a 'ship' -d 'Alias for info'
|
|
453
|
+
complete -c omaship -n '__fish_use_subcommand' -a 'new' -d 'Create and provision a new ship'
|
|
454
|
+
complete -c omaship -n '__fish_use_subcommand' -a 'upgrade' -d 'Open browser to upgrade your plan'
|
|
455
|
+
complete -c omaship -n '__fish_use_subcommand' -a 'logout' -d 'Remove local CLI credentials'
|
|
456
|
+
complete -c omaship -n '__fish_use_subcommand' -a 'complete' -d 'Print shell completion script'
|
|
457
|
+
complete -c omaship -n '__fish_use_subcommand' -a 'help' -d 'Describe available commands'
|
|
431
458
|
|
|
432
459
|
complete -c omaship -n '__fish_seen_subcommand_from login' -l token -d 'API token from omaship settings'
|
|
433
460
|
complete -c omaship -n '__fish_seen_subcommand_from new' -l domain -d 'Root domain'
|
|
434
|
-
complete -c omaship -n '__fish_seen_subcommand_from info status ship
|
|
461
|
+
complete -c omaship -n '__fish_seen_subcommand_from info status ship' -l ship -d 'Ship from omaship list' -a '(__fish_omaship_ship_refs)'
|
|
435
462
|
complete -c omaship -n '__fish_seen_subcommand_from use' -f -a '(__fish_omaship_ship_refs)'
|
|
436
463
|
complete -c omaship -n '__fish_seen_subcommand_from complete' -f -a 'bash zsh fish'
|
|
437
464
|
FISH
|
|
@@ -441,13 +468,62 @@ module Omaship
|
|
|
441
468
|
build_api_client(token: token).authenticate
|
|
442
469
|
end
|
|
443
470
|
|
|
444
|
-
def
|
|
445
|
-
|
|
446
|
-
|
|
471
|
+
def can_create_ship_from_auth?(auth)
|
|
472
|
+
can_create_ship = auth.dig("account", "can_create_ship")
|
|
473
|
+
return can_create_ship unless can_create_ship.nil?
|
|
474
|
+
|
|
475
|
+
legacy_plan = auth.dig("user", "plan").to_s
|
|
476
|
+
return false if legacy_plan.empty?
|
|
477
|
+
|
|
478
|
+
if legacy_plan == "free"
|
|
479
|
+
auth.dig("account", "ship_count").to_i < 1
|
|
480
|
+
else
|
|
481
|
+
true
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def allowed_starter_kinds_from_auth(auth)
|
|
486
|
+
allowed_starter_kinds = Array(auth.dig("account", "allowed_starter_kinds"))
|
|
487
|
+
.map(&:to_s)
|
|
488
|
+
.reject(&:empty?)
|
|
489
|
+
.uniq
|
|
490
|
+
|
|
491
|
+
return allowed_starter_kinds if allowed_starter_kinds.any?
|
|
492
|
+
|
|
493
|
+
legacy_plan = auth.dig("user", "plan").to_s
|
|
494
|
+
|
|
495
|
+
if legacy_plan == "free"
|
|
496
|
+
[ "landing" ]
|
|
497
|
+
elsif legacy_plan.empty?
|
|
498
|
+
[]
|
|
499
|
+
else
|
|
500
|
+
%w[landing app api]
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def create_paid_ship(name:, token:, starter_kind:, auth:)
|
|
505
|
+
identifier_suffix = generated_identifier_suffix
|
|
506
|
+
root_domain = requested_root_domain
|
|
507
|
+
harbor_id = choose_harbor_id(auth: auth)
|
|
508
|
+
payload = build_api_client(token: token).create_ship(
|
|
509
|
+
name: name,
|
|
510
|
+
harbor_id: harbor_id,
|
|
511
|
+
root_domain: root_domain,
|
|
512
|
+
bring_own_domain: root_domain.empty? ? "0" : "1",
|
|
513
|
+
identifier_suffix: identifier_suffix,
|
|
514
|
+
starter_kind: starter_kind
|
|
515
|
+
)
|
|
447
516
|
ship = payload.fetch("ship")
|
|
517
|
+
progress_renderer.start_activity(
|
|
518
|
+
"Setting up #{ship_display_name(ship, fallback: name, suffix: identifier_suffix)}",
|
|
519
|
+
eta: DEFAULT_PROVISIONING_ETA
|
|
520
|
+
)
|
|
448
521
|
|
|
449
|
-
|
|
450
|
-
|
|
522
|
+
begin
|
|
523
|
+
final_ship = poll_until_terminal(ship_id: ship.fetch("id"), token: token)
|
|
524
|
+
ensure
|
|
525
|
+
progress_renderer.finish_activity
|
|
526
|
+
end
|
|
451
527
|
|
|
452
528
|
if final_ship.fetch("status") == "live"
|
|
453
529
|
progress_renderer.step("Ready. Customers can sign up and pay at #{final_ship.fetch("root_domain")}")
|
|
@@ -457,15 +533,42 @@ module Omaship
|
|
|
457
533
|
end
|
|
458
534
|
end
|
|
459
535
|
|
|
460
|
-
def
|
|
461
|
-
|
|
536
|
+
def choose_paid_starter_kind
|
|
537
|
+
say
|
|
538
|
+
say "--- Ship Type ---"
|
|
539
|
+
selected = ship_type_picker.pick
|
|
540
|
+
say "Selected: #{Omaship::ShipTypePicker.label_for(selected)}"
|
|
541
|
+
selected
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def choose_harbor_id(auth:)
|
|
545
|
+
ready_harbors = ready_harbors_from_auth(auth)
|
|
546
|
+
|
|
547
|
+
if ready_harbors.empty?
|
|
548
|
+
raise Thor::Error, "No ready harbor available. Provision a managed harbor in Omaship before launching a full app or API."
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
say
|
|
552
|
+
say "--- Harbor ---"
|
|
553
|
+
selected = harbor_picker(ready_harbors).pick
|
|
554
|
+
say "Selected harbor: #{harbor_label(ready_harbors, selected)}"
|
|
555
|
+
selected
|
|
556
|
+
end
|
|
462
557
|
|
|
463
|
-
|
|
464
|
-
|
|
558
|
+
def create_paid_landingpage(name:, token:)
|
|
559
|
+
create_landingpage_ship(name: name, token: token, show_free_banner: false)
|
|
560
|
+
end
|
|
465
561
|
|
|
466
|
-
|
|
467
|
-
|
|
562
|
+
def create_free_ship(name:, token:)
|
|
563
|
+
create_landingpage_ship(name: name, token: token, show_free_banner: true)
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def create_landingpage_ship(name:, token:, show_free_banner:)
|
|
567
|
+
if show_free_banner
|
|
568
|
+
print_free_banner
|
|
468
569
|
end
|
|
570
|
+
|
|
571
|
+
purpose_profile = collect_purpose_profile
|
|
469
572
|
color_scheme = pick_color_scheme
|
|
470
573
|
|
|
471
574
|
say
|
|
@@ -484,8 +587,8 @@ module Omaship
|
|
|
484
587
|
end
|
|
485
588
|
|
|
486
589
|
say
|
|
487
|
-
say "Live at:
|
|
488
|
-
say "Review and refine at: #{
|
|
590
|
+
say "Live at: #{landingpage_url(ship)}"
|
|
591
|
+
say "Review and refine at: #{ship_dashboard_url(ship)}"
|
|
489
592
|
end
|
|
490
593
|
|
|
491
594
|
def print_free_banner
|
|
@@ -510,24 +613,19 @@ module Omaship
|
|
|
510
613
|
say
|
|
511
614
|
end
|
|
512
615
|
|
|
513
|
-
def ask_purpose_profile?
|
|
514
|
-
say "Would you like to create a purpose profile?"
|
|
515
|
-
say "This helps generate better landing page copy. (5 questions, ~2 min)"
|
|
516
|
-
say
|
|
517
|
-
answer = ask("Continue with purpose profile? (Y/n):")
|
|
518
|
-
answer.to_s.strip.downcase != "n"
|
|
519
|
-
end
|
|
520
|
-
|
|
521
616
|
def collect_purpose_profile
|
|
617
|
+
say
|
|
618
|
+
say "We'll start with a purpose profile."
|
|
619
|
+
say "This helps generate better landing page copy. (5 questions, ~2 min)"
|
|
522
620
|
say
|
|
523
621
|
say "--- Purpose Profile ---"
|
|
524
622
|
say
|
|
525
623
|
|
|
526
|
-
raw_problem =
|
|
527
|
-
raw_audience =
|
|
528
|
-
raw_belief =
|
|
529
|
-
raw_contribution =
|
|
530
|
-
raw_outcome =
|
|
624
|
+
raw_problem = ask_question("What problem does your product solve?")
|
|
625
|
+
raw_audience = ask_question("Who is most affected by this problem?")
|
|
626
|
+
raw_belief = ask_question("Why is this worth solving? What's your core belief?")
|
|
627
|
+
raw_contribution = ask_question("What's your contribution to the solution?")
|
|
628
|
+
raw_outcome = ask_question("What does the world look like when you succeed?")
|
|
531
629
|
|
|
532
630
|
{
|
|
533
631
|
raw_problem: raw_problem,
|
|
@@ -546,10 +644,64 @@ module Omaship
|
|
|
546
644
|
selected
|
|
547
645
|
end
|
|
548
646
|
|
|
647
|
+
def ask_question(question)
|
|
648
|
+
say question
|
|
649
|
+
ask("> ")
|
|
650
|
+
end
|
|
651
|
+
|
|
549
652
|
def color_picker
|
|
550
653
|
@color_picker ||= Omaship::ColorPicker.new
|
|
551
654
|
end
|
|
552
655
|
|
|
656
|
+
def ship_type_picker
|
|
657
|
+
@ship_type_picker ||= Omaship::ShipTypePicker.new
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
def harbor_picker(harbors)
|
|
661
|
+
Omaship::HarborPicker.new(harbors: harbors)
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
def ready_harbors_from_auth(auth)
|
|
665
|
+
Array(auth.dig("account", "ready_harbors")).map do |harbor|
|
|
666
|
+
{
|
|
667
|
+
"id" => harbor["id"] || harbor[:id],
|
|
668
|
+
"name" => harbor["name"] || harbor[:name],
|
|
669
|
+
"status" => harbor["status"] || harbor[:status],
|
|
670
|
+
"management" => harbor["management"] || harbor[:management]
|
|
671
|
+
}
|
|
672
|
+
end.select { |harbor| harbor["id"] && harbor["name"].to_s.strip != "" }
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
def harbor_label(harbors, harbor_id)
|
|
676
|
+
harbor = harbors.find { |entry| entry["id"].to_s == harbor_id.to_s }
|
|
677
|
+
if harbor
|
|
678
|
+
harbor["name"].to_s
|
|
679
|
+
else
|
|
680
|
+
harbor_id.to_s
|
|
681
|
+
end
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
def landingpage_url(ship)
|
|
685
|
+
root_domain = ship.fetch("root_domain").to_s.strip
|
|
686
|
+
return "" if root_domain.empty?
|
|
687
|
+
|
|
688
|
+
base_url = URI.parse(resolved_host)
|
|
689
|
+
scheme = base_url.scheme.to_s.strip
|
|
690
|
+
scheme = "https" if scheme.empty?
|
|
691
|
+
|
|
692
|
+
port = base_url.port
|
|
693
|
+
default_port = scheme == "https" ? 443 : 80
|
|
694
|
+
port_suffix = port == default_port ? "" : ":#{port}"
|
|
695
|
+
|
|
696
|
+
"#{scheme}://#{root_domain}#{port_suffix}/"
|
|
697
|
+
rescue URI::InvalidURIError
|
|
698
|
+
"https://#{root_domain}/"
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def ship_dashboard_url(ship)
|
|
702
|
+
"#{resolved_host.to_s.sub(%r{/*$}, "")}/ships/#{ship.fetch("id")}"
|
|
703
|
+
end
|
|
704
|
+
|
|
553
705
|
def poll_until_distilled(ship_id:, token:)
|
|
554
706
|
client = build_api_client(token: token)
|
|
555
707
|
30.times do
|
|
@@ -585,7 +737,7 @@ module Omaship
|
|
|
585
737
|
|
|
586
738
|
def resolve_ship_without_explicit_selection(token:)
|
|
587
739
|
ships = build_api_client(token: token).list_ships
|
|
588
|
-
default_ship_reference =
|
|
740
|
+
default_ship_reference = current_default_ship_reference(token: token, ships: ships)
|
|
589
741
|
|
|
590
742
|
if !default_ship_reference.empty?
|
|
591
743
|
default_ship = match_ship(ships: ships, ship_reference: default_ship_reference)
|
|
@@ -615,7 +767,7 @@ module Omaship
|
|
|
615
767
|
if ship
|
|
616
768
|
ship
|
|
617
769
|
else
|
|
618
|
-
raise Thor::Error, "Unknown ship `#{ship_reference}`. Run `omaship list` and use
|
|
770
|
+
raise Thor::Error, "Unknown ship `#{ship_reference}`. Run `omaship list` and use the ship domain from the list, or the numeric id."
|
|
619
771
|
end
|
|
620
772
|
end
|
|
621
773
|
|
|
@@ -626,27 +778,15 @@ module Omaship
|
|
|
626
778
|
end
|
|
627
779
|
|
|
628
780
|
def default_ship_match?(ship:, ship_reference:)
|
|
629
|
-
ship.fetch("id").to_s == ship_reference ||
|
|
781
|
+
ship.fetch("id").to_s == ship_reference ||
|
|
782
|
+
ship.fetch("reference") == ship_reference ||
|
|
783
|
+
ship["full_name"] == ship_reference
|
|
630
784
|
end
|
|
631
785
|
|
|
632
786
|
def persist_default_ship(ship)
|
|
633
787
|
credentials.write_default_ship(ship.fetch("reference"))
|
|
634
788
|
end
|
|
635
789
|
|
|
636
|
-
def resolve_ship_for_deploy(token:)
|
|
637
|
-
resolve_ship(token: token)
|
|
638
|
-
rescue Thor::Error => error
|
|
639
|
-
if options[:ship].to_s.strip.empty? && selection_error_message?(error.message)
|
|
640
|
-
raise Thor::Error, "#{error.message} `omaship deploy` also requires a full-access token."
|
|
641
|
-
end
|
|
642
|
-
|
|
643
|
-
raise
|
|
644
|
-
end
|
|
645
|
-
|
|
646
|
-
def selection_error_message?(message)
|
|
647
|
-
message == no_ships_message || message == multiple_ships_message
|
|
648
|
-
end
|
|
649
|
-
|
|
650
790
|
def no_ships_message
|
|
651
791
|
"No ships available yet. Run `omaship new <name>` first."
|
|
652
792
|
end
|
|
@@ -670,8 +810,6 @@ module Omaship
|
|
|
670
810
|
"This onboarding token only supports `omaship new` while you create your first ship. Create a full-access token in Settings after onboarding to use other CLI commands."
|
|
671
811
|
elsif command == :new_ship
|
|
672
812
|
"`omaship new` requires a full-access token. Create a token with Full CLI access and run `omaship login` again."
|
|
673
|
-
elsif command == :deploy
|
|
674
|
-
"`omaship deploy` requires a full-access token. Create a token with Full CLI access and run `omaship login` again."
|
|
675
813
|
else
|
|
676
814
|
"Permission denied for this action. Check your token scopes and run `omaship login` again."
|
|
677
815
|
end
|
|
@@ -702,8 +840,9 @@ module Omaship
|
|
|
702
840
|
client = build_api_client(token: token)
|
|
703
841
|
90.times do
|
|
704
842
|
ship_payload = client.ship(ship_id: ship_id).fetch("ship")
|
|
843
|
+
update_provisioning_activity(client: client, ship_payload: ship_payload)
|
|
705
844
|
status = ship_payload.fetch("status")
|
|
706
|
-
if %w[live error].include?(status)
|
|
845
|
+
if %w[live error deploy_failed].include?(status)
|
|
707
846
|
return ship_payload
|
|
708
847
|
end
|
|
709
848
|
sleep 2
|
|
@@ -712,53 +851,71 @@ module Omaship
|
|
|
712
851
|
raise Thor::Error, "Provisioning timed out."
|
|
713
852
|
end
|
|
714
853
|
|
|
715
|
-
def
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
deploy_payload = client.latest_deploy(ship_id: ship_id).fetch("deploy")
|
|
720
|
-
status = deploy_payload.fetch("status")
|
|
721
|
-
conclusion = deploy_payload["conclusion"]
|
|
722
|
-
if status != last_status
|
|
723
|
-
emit_deploy_status(status: status)
|
|
724
|
-
last_status = status
|
|
725
|
-
end
|
|
854
|
+
def update_provisioning_activity(client:, ship_payload:)
|
|
855
|
+
latest_voyage = ship_payload["latest_voyage"]
|
|
856
|
+
voyage_id = latest_voyage && latest_voyage["id"]
|
|
857
|
+
return if voyage_id.to_s.strip.empty?
|
|
726
858
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
859
|
+
voyage_payload = client.voyage(voyage_id: voyage_id).fetch("voyage")
|
|
860
|
+
progress_renderer.update_activity(
|
|
861
|
+
message: provisioning_message_for(voyage_payload: voyage_payload),
|
|
862
|
+
eta: provisioning_eta_for(voyage_payload: voyage_payload)
|
|
863
|
+
)
|
|
864
|
+
rescue Omaship::ApiClient::Error
|
|
865
|
+
end
|
|
734
866
|
|
|
735
|
-
|
|
736
|
-
|
|
867
|
+
def provisioning_message_for(voyage_payload:)
|
|
868
|
+
step = current_voyage_step(voyage_payload:)
|
|
737
869
|
|
|
738
|
-
|
|
870
|
+
if step
|
|
871
|
+
PROVISION_STEP_MESSAGES.fetch(step.fetch("step_key"), "Setting up your codebase")
|
|
872
|
+
elsif voyage_payload["status"] == "queued"
|
|
873
|
+
"Queueing your provisioning run"
|
|
874
|
+
else
|
|
875
|
+
"Setting up your codebase"
|
|
876
|
+
end
|
|
739
877
|
end
|
|
740
878
|
|
|
741
|
-
def
|
|
742
|
-
|
|
743
|
-
conclusion = deploy_payload["conclusion"].to_s.strip
|
|
879
|
+
def provisioning_eta_for(voyage_payload:)
|
|
880
|
+
step = current_voyage_step(voyage_payload:)
|
|
744
881
|
|
|
745
|
-
if
|
|
746
|
-
|
|
882
|
+
if step
|
|
883
|
+
PROVISION_STEP_ETAS.fetch(step.fetch("step_key"), DEFAULT_PROVISIONING_ETA)
|
|
747
884
|
else
|
|
748
|
-
|
|
885
|
+
DEFAULT_PROVISIONING_ETA
|
|
749
886
|
end
|
|
750
887
|
end
|
|
751
888
|
|
|
752
|
-
def
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
889
|
+
def current_voyage_step(voyage_payload:)
|
|
890
|
+
steps = Array(voyage_payload["steps"])
|
|
891
|
+
running_step = steps.reverse.find { |step| step["status"] == "running" }
|
|
892
|
+
return running_step if running_step
|
|
893
|
+
|
|
894
|
+
queued_step = steps.reverse.find { |step| step["status"] == "queued" }
|
|
895
|
+
return queued_step if queued_step
|
|
896
|
+
|
|
897
|
+
steps.last
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
def requested_root_domain
|
|
901
|
+
options[:domain].to_s.strip.downcase
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
def generated_identifier_suffix
|
|
905
|
+
SecureRandom.hex(2)
|
|
906
|
+
end
|
|
907
|
+
|
|
908
|
+
def ship_display_name(ship, fallback:, suffix:)
|
|
909
|
+
display_name = ship["display_name"].to_s.strip
|
|
910
|
+
return display_name unless display_name.empty?
|
|
911
|
+
|
|
912
|
+
base_name = ship["name"].to_s.strip
|
|
913
|
+
return base_name unless base_name.empty?
|
|
914
|
+
|
|
915
|
+
normalized_fallback = fallback.to_s.parameterize
|
|
916
|
+
return normalized_fallback if normalized_fallback.end_with?("-#{suffix}")
|
|
917
|
+
|
|
918
|
+
"#{normalized_fallback}-#{suffix}"
|
|
762
919
|
end
|
|
763
920
|
|
|
764
921
|
def open_browser(url)
|
|
@@ -781,8 +938,75 @@ module Omaship
|
|
|
781
938
|
end
|
|
782
939
|
end
|
|
783
940
|
|
|
784
|
-
def
|
|
785
|
-
|
|
941
|
+
def current_default_ship_reference(token:, ships: nil)
|
|
942
|
+
default_ship_reference = credentials.default_ship.to_s.strip
|
|
943
|
+
return "" if default_ship_reference.empty?
|
|
944
|
+
|
|
945
|
+
if ships
|
|
946
|
+
normalize_default_ship_reference(default_ship_reference:, ships:)
|
|
947
|
+
elsif legacy_default_ship_reference?(default_ship_reference)
|
|
948
|
+
normalize_default_ship_reference(
|
|
949
|
+
default_ship_reference:,
|
|
950
|
+
ships: build_api_client(token: token).list_ships
|
|
951
|
+
)
|
|
952
|
+
else
|
|
953
|
+
default_ship_reference
|
|
954
|
+
end
|
|
955
|
+
end
|
|
956
|
+
|
|
957
|
+
def normalize_default_ship_reference(default_ship_reference:, ships:)
|
|
958
|
+
ship = match_ship(ships:, ship_reference: default_ship_reference)
|
|
959
|
+
|
|
960
|
+
if ship
|
|
961
|
+
normalized_reference = ship.fetch("reference")
|
|
962
|
+
|
|
963
|
+
if normalized_reference != default_ship_reference
|
|
964
|
+
credentials.write_default_ship(normalized_reference)
|
|
965
|
+
end
|
|
966
|
+
|
|
967
|
+
normalized_reference
|
|
968
|
+
else
|
|
969
|
+
credentials.clear_default_ship
|
|
970
|
+
""
|
|
971
|
+
end
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
def legacy_default_ship_reference?(default_ship_reference)
|
|
975
|
+
default_ship_reference.include?("/")
|
|
976
|
+
end
|
|
977
|
+
|
|
978
|
+
def normalized_plan_label(plan:, admin: false)
|
|
979
|
+
return "Master" if admin
|
|
980
|
+
|
|
981
|
+
value = plan.to_s.strip
|
|
982
|
+
|
|
983
|
+
if value.empty?
|
|
984
|
+
"Unknown"
|
|
985
|
+
else
|
|
986
|
+
value.split(/[_-]/).map(&:capitalize).join(" ")
|
|
987
|
+
end
|
|
988
|
+
end
|
|
989
|
+
|
|
990
|
+
def ship_list_line(ship:, marker:)
|
|
991
|
+
line = "#{marker} #{ship.fetch("id")} #{ship.fetch("reference")}"
|
|
992
|
+
display_name = ship.fetch("display_name").to_s.strip
|
|
993
|
+
|
|
994
|
+
if !display_name.empty? && display_name != ship.fetch("reference")
|
|
995
|
+
line = "#{line} #{display_name}"
|
|
996
|
+
end
|
|
997
|
+
|
|
998
|
+
"#{line} #{ship.fetch("status")} #{ship.fetch("primary_url", "-") || "-"}"
|
|
999
|
+
end
|
|
1000
|
+
|
|
1001
|
+
def latest_voyage_summary_for(ship:)
|
|
1002
|
+
voyage = ship["latest_voyage"]
|
|
1003
|
+
return unless voyage
|
|
1004
|
+
|
|
1005
|
+
{
|
|
1006
|
+
id: voyage["id"],
|
|
1007
|
+
kind: voyage["kind"],
|
|
1008
|
+
status: voyage["status"]
|
|
1009
|
+
}
|
|
786
1010
|
end
|
|
787
1011
|
end
|
|
788
1012
|
end
|