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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -0
- data/README.md +22 -8
- data/lib/omaship/api_client.rb +76 -17
- data/lib/omaship/cli.rb +396 -160
- 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 +6 -3
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,8 +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
|
-
|
|
147
|
-
say "#{marker} #{ship.fetch("id")} #{ship.fetch("full_name")} #{ship.fetch("status")} #{app_url}"
|
|
169
|
+
say ship_list_line(ship:, marker:)
|
|
148
170
|
end
|
|
149
171
|
|
|
150
172
|
if !default_ship_reference.empty?
|
|
@@ -160,11 +182,11 @@ module Omaship
|
|
|
160
182
|
Set the default ship used by commands when `--ship` is omitted.
|
|
161
183
|
|
|
162
184
|
Get available values with `omaship list`.
|
|
163
|
-
|
|
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)
|
|
@@ -173,67 +195,67 @@ module Omaship
|
|
|
173
195
|
ship = find_ship_by_reference(token: token, ship_reference: ship_reference)
|
|
174
196
|
persist_default_ship(ship)
|
|
175
197
|
|
|
176
|
-
say "Default ship set to #{ship.fetch("
|
|
198
|
+
say "Default ship set to #{ship.fetch("reference")}."
|
|
177
199
|
end
|
|
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
|
|
185
207
|
client = build_api_client(token: token)
|
|
186
208
|
resolved_ship = resolve_ship(token: token)
|
|
187
209
|
ship = client.ship(ship_id: resolved_ship.fetch("id")).fetch("ship")
|
|
188
|
-
deploy = client.latest_deploy(ship_id: ship.fetch("id")).fetch("deploy")
|
|
189
210
|
default_ship_reference = credentials.default_ship.to_s.strip
|
|
211
|
+
latest_voyage = latest_voyage_summary_for(ship:)
|
|
190
212
|
|
|
191
|
-
say "Ship: #{ship.fetch("
|
|
213
|
+
say "Ship: #{ship.fetch("display_name")}"
|
|
192
214
|
say "ID: #{ship.fetch("id")}"
|
|
193
215
|
say "Status: #{ship.fetch("status")}"
|
|
194
|
-
say "
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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}"
|
|
200
224
|
end
|
|
201
|
-
|
|
202
|
-
|
|
225
|
+
say "Default Ship: #{default_ship_reference.empty? ? "-" : default_ship_reference}"
|
|
226
|
+
|
|
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)}"
|
|
231
|
+
end
|
|
203
232
|
end
|
|
204
233
|
end
|
|
205
234
|
end
|
|
206
235
|
|
|
207
236
|
desc "new NAME", "Create and provision a new ship"
|
|
208
|
-
method_option :domain, type: :string, desc: "
|
|
209
|
-
method_option :skip_purpose, type: :boolean, default: false, desc: "Skip purpose profile questions"
|
|
237
|
+
method_option :domain, type: :string, desc: "Custom root domain"
|
|
210
238
|
def new_ship(name)
|
|
211
239
|
with_api_error_handling(command: :new_ship) do
|
|
212
240
|
token = current_token
|
|
213
|
-
|
|
241
|
+
auth = fetch_auth(token: token)
|
|
242
|
+
allowed_starter_kinds = allowed_starter_kinds_from_auth(auth)
|
|
214
243
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
else
|
|
218
|
-
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."
|
|
219
246
|
end
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
247
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
token = current_token
|
|
228
|
-
ship = resolve_ship_for_deploy(token: token)
|
|
229
|
-
ship_name = ship.fetch("full_name", "##{ship.fetch("id")}")
|
|
230
|
-
|
|
231
|
-
progress_renderer.step("Target ship: #{ship_name}.")
|
|
232
|
-
|
|
233
|
-
build_api_client(token: token).create_deploy(ship_id: ship.fetch("id"))
|
|
234
|
-
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
|
|
235
252
|
|
|
236
|
-
|
|
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
|
|
237
259
|
end
|
|
238
260
|
end
|
|
239
261
|
|
|
@@ -295,7 +317,7 @@ module Omaship
|
|
|
295
317
|
fi
|
|
296
318
|
|
|
297
319
|
if [[ ${COMP_CWORD} -eq 1 ]]; then
|
|
298
|
-
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}") )
|
|
299
321
|
return 0
|
|
300
322
|
fi
|
|
301
323
|
|
|
@@ -324,11 +346,11 @@ module Omaship
|
|
|
324
346
|
use)
|
|
325
347
|
COMPREPLY=( $(compgen -W "--host -h --help" -- "${cur}") )
|
|
326
348
|
;;
|
|
327
|
-
info|status|ship
|
|
349
|
+
info|status|ship)
|
|
328
350
|
COMPREPLY=( $(compgen -W "--ship --host -h --help" -- "${cur}") )
|
|
329
351
|
;;
|
|
330
352
|
new)
|
|
331
|
-
COMPREPLY=( $(compgen -W "--domain --
|
|
353
|
+
COMPREPLY=( $(compgen -W "--domain --host -h --help" -- "${cur}") )
|
|
332
354
|
;;
|
|
333
355
|
complete)
|
|
334
356
|
COMPREPLY=( $(compgen -W "bash zsh fish" -- "${cur}") )
|
|
@@ -366,7 +388,6 @@ module Omaship
|
|
|
366
388
|
'status:Alias for info'
|
|
367
389
|
'ship:Alias for info'
|
|
368
390
|
'new:Create and provision a new ship'
|
|
369
|
-
'deploy:Deploy a ship'
|
|
370
391
|
'upgrade:Open browser to upgrade your plan'
|
|
371
392
|
'logout:Remove local CLI credentials'
|
|
372
393
|
'complete:Print shell completion script'
|
|
@@ -393,11 +414,11 @@ module Omaship
|
|
|
393
414
|
use)
|
|
394
415
|
_arguments '--host[API host]:host:' '1:ship reference:_omaship_ship_refs'
|
|
395
416
|
;;
|
|
396
|
-
info|status|ship
|
|
417
|
+
info|status|ship)
|
|
397
418
|
_arguments '--ship[Ship from omaship list]:ship:_omaship_ship_refs' '--host[API host]:host:'
|
|
398
419
|
;;
|
|
399
420
|
new)
|
|
400
|
-
_arguments '--domain[Root domain]:domain:' '--
|
|
421
|
+
_arguments '--domain[Root domain]:domain:' '--host[API host]:host:' '1:name:'
|
|
401
422
|
;;
|
|
402
423
|
complete)
|
|
403
424
|
_arguments '1:shell:(bash zsh fish)'
|
|
@@ -421,30 +442,88 @@ module Omaship
|
|
|
421
442
|
end
|
|
422
443
|
|
|
423
444
|
complete -c omaship -f
|
|
424
|
-
complete -c omaship -n '__fish_use_subcommand' -a 'login whoami list use info status ship new deploy upgrade logout complete help'
|
|
425
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'
|
|
426
458
|
|
|
427
459
|
complete -c omaship -n '__fish_seen_subcommand_from login' -l token -d 'API token from omaship settings'
|
|
428
460
|
complete -c omaship -n '__fish_seen_subcommand_from new' -l domain -d 'Root domain'
|
|
429
|
-
complete -c omaship -n '__fish_seen_subcommand_from
|
|
430
|
-
complete -c omaship -n '__fish_seen_subcommand_from info status ship deploy' -l ship -d 'Ship from omaship list' -a '(__fish_omaship_ship_refs)'
|
|
461
|
+
complete -c omaship -n '__fish_seen_subcommand_from info status ship' -l ship -d 'Ship from omaship list' -a '(__fish_omaship_ship_refs)'
|
|
431
462
|
complete -c omaship -n '__fish_seen_subcommand_from use' -f -a '(__fish_omaship_ship_refs)'
|
|
432
463
|
complete -c omaship -n '__fish_seen_subcommand_from complete' -f -a 'bash zsh fish'
|
|
433
464
|
FISH
|
|
434
465
|
end
|
|
435
466
|
|
|
436
|
-
def
|
|
437
|
-
|
|
438
|
-
|
|
467
|
+
def fetch_auth(token:)
|
|
468
|
+
build_api_client(token: token).authenticate
|
|
469
|
+
end
|
|
470
|
+
|
|
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
|
|
439
502
|
end
|
|
440
503
|
|
|
441
|
-
def create_paid_ship(name:, token:)
|
|
442
|
-
|
|
443
|
-
|
|
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
|
+
)
|
|
444
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
|
+
)
|
|
445
521
|
|
|
446
|
-
|
|
447
|
-
|
|
522
|
+
begin
|
|
523
|
+
final_ship = poll_until_terminal(ship_id: ship.fetch("id"), token: token)
|
|
524
|
+
ensure
|
|
525
|
+
progress_renderer.finish_activity
|
|
526
|
+
end
|
|
448
527
|
|
|
449
528
|
if final_ship.fetch("status") == "live"
|
|
450
529
|
progress_renderer.step("Ready. Customers can sign up and pay at #{final_ship.fetch("root_domain")}")
|
|
@@ -454,23 +533,48 @@ module Omaship
|
|
|
454
533
|
end
|
|
455
534
|
end
|
|
456
535
|
|
|
457
|
-
def
|
|
458
|
-
|
|
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
|
|
459
543
|
|
|
460
|
-
|
|
461
|
-
|
|
544
|
+
def choose_harbor_id(auth:)
|
|
545
|
+
ready_harbors = ready_harbors_from_auth(auth)
|
|
462
546
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
purpose_profile = collect_purpose_profile
|
|
466
|
-
end
|
|
467
|
-
color_scheme = pick_color_scheme
|
|
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."
|
|
468
549
|
end
|
|
469
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
|
|
557
|
+
|
|
558
|
+
def create_paid_landingpage(name:, token:)
|
|
559
|
+
create_landingpage_ship(name: name, token: token, show_free_banner: false)
|
|
560
|
+
end
|
|
561
|
+
|
|
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
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
purpose_profile = collect_purpose_profile
|
|
572
|
+
color_scheme = pick_color_scheme
|
|
573
|
+
|
|
470
574
|
say
|
|
471
575
|
progress_renderer.step("Creating your landing page...")
|
|
472
576
|
|
|
473
|
-
payload = build_api_client(token: token).
|
|
577
|
+
payload = build_api_client(token: token).create_landingpage(
|
|
474
578
|
name: name,
|
|
475
579
|
color_scheme: color_scheme,
|
|
476
580
|
purpose_profile: purpose_profile
|
|
@@ -483,8 +587,8 @@ module Omaship
|
|
|
483
587
|
end
|
|
484
588
|
|
|
485
589
|
say
|
|
486
|
-
say "Live at:
|
|
487
|
-
say "Review and refine at: #{
|
|
590
|
+
say "Live at: #{landingpage_url(ship)}"
|
|
591
|
+
say "Review and refine at: #{ship_dashboard_url(ship)}"
|
|
488
592
|
end
|
|
489
593
|
|
|
490
594
|
def print_free_banner
|
|
@@ -509,24 +613,19 @@ module Omaship
|
|
|
509
613
|
say
|
|
510
614
|
end
|
|
511
615
|
|
|
512
|
-
def ask_purpose_profile?
|
|
513
|
-
say "Would you like to create a purpose profile?"
|
|
514
|
-
say "This helps generate better landing page copy. (5 questions, ~2 min)"
|
|
515
|
-
say
|
|
516
|
-
answer = ask("Continue with purpose profile? (Y/n):")
|
|
517
|
-
answer.to_s.strip.downcase != "n"
|
|
518
|
-
end
|
|
519
|
-
|
|
520
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)"
|
|
521
620
|
say
|
|
522
621
|
say "--- Purpose Profile ---"
|
|
523
622
|
say
|
|
524
623
|
|
|
525
|
-
raw_problem =
|
|
526
|
-
raw_audience =
|
|
527
|
-
raw_belief =
|
|
528
|
-
raw_contribution =
|
|
529
|
-
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?")
|
|
530
629
|
|
|
531
630
|
{
|
|
532
631
|
raw_problem: raw_problem,
|
|
@@ -545,10 +644,64 @@ module Omaship
|
|
|
545
644
|
selected
|
|
546
645
|
end
|
|
547
646
|
|
|
647
|
+
def ask_question(question)
|
|
648
|
+
say question
|
|
649
|
+
ask("> ")
|
|
650
|
+
end
|
|
651
|
+
|
|
548
652
|
def color_picker
|
|
549
653
|
@color_picker ||= Omaship::ColorPicker.new
|
|
550
654
|
end
|
|
551
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
|
+
|
|
552
705
|
def poll_until_distilled(ship_id:, token:)
|
|
553
706
|
client = build_api_client(token: token)
|
|
554
707
|
30.times do
|
|
@@ -584,7 +737,7 @@ module Omaship
|
|
|
584
737
|
|
|
585
738
|
def resolve_ship_without_explicit_selection(token:)
|
|
586
739
|
ships = build_api_client(token: token).list_ships
|
|
587
|
-
default_ship_reference =
|
|
740
|
+
default_ship_reference = current_default_ship_reference(token: token, ships: ships)
|
|
588
741
|
|
|
589
742
|
if !default_ship_reference.empty?
|
|
590
743
|
default_ship = match_ship(ships: ships, ship_reference: default_ship_reference)
|
|
@@ -614,7 +767,7 @@ module Omaship
|
|
|
614
767
|
if ship
|
|
615
768
|
ship
|
|
616
769
|
else
|
|
617
|
-
raise Thor::Error, "Unknown ship `#{ship_reference}`. Run `omaship list` and use the
|
|
770
|
+
raise Thor::Error, "Unknown ship `#{ship_reference}`. Run `omaship list` and use the ship domain from the list, or the numeric id."
|
|
618
771
|
end
|
|
619
772
|
end
|
|
620
773
|
|
|
@@ -625,25 +778,13 @@ module Omaship
|
|
|
625
778
|
end
|
|
626
779
|
|
|
627
780
|
def default_ship_match?(ship:, ship_reference:)
|
|
628
|
-
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
|
|
629
784
|
end
|
|
630
785
|
|
|
631
786
|
def persist_default_ship(ship)
|
|
632
|
-
credentials.write_default_ship(ship.fetch("
|
|
633
|
-
end
|
|
634
|
-
|
|
635
|
-
def resolve_ship_for_deploy(token:)
|
|
636
|
-
resolve_ship(token: token)
|
|
637
|
-
rescue Thor::Error => error
|
|
638
|
-
if options[:ship].to_s.strip.empty? && selection_error_message?(error.message)
|
|
639
|
-
raise Thor::Error, "#{error.message} `omaship deploy` also requires a full-access token."
|
|
640
|
-
end
|
|
641
|
-
|
|
642
|
-
raise
|
|
643
|
-
end
|
|
644
|
-
|
|
645
|
-
def selection_error_message?(message)
|
|
646
|
-
message == no_ships_message || message == multiple_ships_message
|
|
787
|
+
credentials.write_default_ship(ship.fetch("reference"))
|
|
647
788
|
end
|
|
648
789
|
|
|
649
790
|
def no_ships_message
|
|
@@ -651,7 +792,7 @@ module Omaship
|
|
|
651
792
|
end
|
|
652
793
|
|
|
653
794
|
def multiple_ships_message
|
|
654
|
-
"Multiple ships found. Run `omaship list` and `omaship use <
|
|
795
|
+
"Multiple ships found. Run `omaship list` and `omaship use <ship-from-list>` (or a ship id)."
|
|
655
796
|
end
|
|
656
797
|
|
|
657
798
|
def with_api_error_handling(command:)
|
|
@@ -665,15 +806,24 @@ module Omaship
|
|
|
665
806
|
end
|
|
666
807
|
|
|
667
808
|
def permission_denied_message(command:)
|
|
668
|
-
if
|
|
809
|
+
if current_access_level == "onboarding"
|
|
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."
|
|
811
|
+
elsif command == :new_ship
|
|
669
812
|
"`omaship new` requires a full-access token. Create a token with Full CLI access and run `omaship login` again."
|
|
670
|
-
elsif command == :deploy
|
|
671
|
-
"`omaship deploy` requires a full-access token. Create a token with Full CLI access and run `omaship login` again."
|
|
672
813
|
else
|
|
673
814
|
"Permission denied for this action. Check your token scopes and run `omaship login` again."
|
|
674
815
|
end
|
|
675
816
|
end
|
|
676
817
|
|
|
818
|
+
def current_access_level
|
|
819
|
+
token = credentials.token
|
|
820
|
+
return "unknown" if token.to_s.strip.empty?
|
|
821
|
+
|
|
822
|
+
build_api_client(token: token).authenticate.dig("token", "access_level").to_s
|
|
823
|
+
rescue NoMethodError, Omaship::ApiClient::Error
|
|
824
|
+
"unknown"
|
|
825
|
+
end
|
|
826
|
+
|
|
677
827
|
def build_api_client(token:)
|
|
678
828
|
Omaship::ApiClient.new(host: resolved_host, token: token)
|
|
679
829
|
end
|
|
@@ -690,8 +840,9 @@ module Omaship
|
|
|
690
840
|
client = build_api_client(token: token)
|
|
691
841
|
90.times do
|
|
692
842
|
ship_payload = client.ship(ship_id: ship_id).fetch("ship")
|
|
843
|
+
update_provisioning_activity(client: client, ship_payload: ship_payload)
|
|
693
844
|
status = ship_payload.fetch("status")
|
|
694
|
-
if %w[live error].include?(status)
|
|
845
|
+
if %w[live error deploy_failed].include?(status)
|
|
695
846
|
return ship_payload
|
|
696
847
|
end
|
|
697
848
|
sleep 2
|
|
@@ -700,53 +851,71 @@ module Omaship
|
|
|
700
851
|
raise Thor::Error, "Provisioning timed out."
|
|
701
852
|
end
|
|
702
853
|
|
|
703
|
-
def
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
deploy_payload = client.latest_deploy(ship_id: ship_id).fetch("deploy")
|
|
708
|
-
status = deploy_payload.fetch("status")
|
|
709
|
-
conclusion = deploy_payload["conclusion"]
|
|
710
|
-
if status != last_status
|
|
711
|
-
emit_deploy_status(status: status)
|
|
712
|
-
last_status = status
|
|
713
|
-
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?
|
|
714
858
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
|
722
866
|
|
|
723
|
-
|
|
724
|
-
|
|
867
|
+
def provisioning_message_for(voyage_payload:)
|
|
868
|
+
step = current_voyage_step(voyage_payload:)
|
|
725
869
|
|
|
726
|
-
|
|
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
|
|
727
877
|
end
|
|
728
878
|
|
|
729
|
-
def
|
|
730
|
-
|
|
731
|
-
conclusion = deploy_payload["conclusion"].to_s.strip
|
|
879
|
+
def provisioning_eta_for(voyage_payload:)
|
|
880
|
+
step = current_voyage_step(voyage_payload:)
|
|
732
881
|
|
|
733
|
-
if
|
|
734
|
-
|
|
882
|
+
if step
|
|
883
|
+
PROVISION_STEP_ETAS.fetch(step.fetch("step_key"), DEFAULT_PROVISIONING_ETA)
|
|
735
884
|
else
|
|
736
|
-
|
|
885
|
+
DEFAULT_PROVISIONING_ETA
|
|
737
886
|
end
|
|
738
887
|
end
|
|
739
888
|
|
|
740
|
-
def
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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}"
|
|
750
919
|
end
|
|
751
920
|
|
|
752
921
|
def open_browser(url)
|
|
@@ -769,8 +938,75 @@ module Omaship
|
|
|
769
938
|
end
|
|
770
939
|
end
|
|
771
940
|
|
|
772
|
-
def
|
|
773
|
-
|
|
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
|
+
}
|
|
774
1010
|
end
|
|
775
1011
|
end
|
|
776
1012
|
end
|