omaship 0.2.2 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f76522b3b89f67ab222e30cbd3055791f747ccd84a3c1121325a2b881b23f88
4
- data.tar.gz: ed00630e44a0691758648ef290d9ade09d2633f1df723cd526268b1a8f103395
3
+ metadata.gz: 6cb3e55ca287cec243c7b73d01ecbd5d37b5830fed0707175e3fce4c92168008
4
+ data.tar.gz: 7f38d4db65faaf778be3af3dd13472793f0d6bc396d82d040a20b39e7c770e13
5
5
  SHA512:
6
- metadata.gz: 21cf9fbd52bfeb7cb833f91894018ffe9c4d22a26efebff655ae609e28ab7ce05256af8ccaf9e767844427f9a6942165c4e5a96e64253a7c045ec9f336234c8d
7
- data.tar.gz: be9499fdf4ca57543abaf8411b5a90ec4495dc5aea79a8fbcaf78a3fc7c7b837fd1c41e6d61abd0b31c2ef879a9a3348c32c137919a65c32fb3adafc0b2987c1
6
+ metadata.gz: 0f3aba37dd0ab910fb8132e192c9f14dab1678982723c9805d18cd1a2e47e242f5806ce25c847ec38bdb05fa54105f59eec55888c6ad6d8d633e5fe0a6d71636
7
+ data.tar.gz: 50fc535b1dea46931d96d8e17f894e94af3e6b8a035137aa7b5425f4bdbc8a07df23ec386affff5abc5b3641c2eabf6ea5c140a0c1508c738504607e0d0d734d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.0](https://github.com/bloomedai/omaship/compare/omaship/v0.4.0...omaship/v0.5.0) (2026-04-04)
4
+
5
+
6
+ ### Features
7
+
8
+ * ✨ enforce cli semver gate ([39952c0](https://github.com/bloomedai/omaship/commit/39952c0e855043acd1f1bcb3c4f963457032de4c))
9
+ * adopt run-first ship flow and terminology ([#381](https://github.com/bloomedai/omaship/issues/381)) ([8bc857f](https://github.com/bloomedai/omaship/commit/8bc857fe3fcec6213f7d9d90fc9e8511411c8b00))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * 🐛 align onboarding cli flow ([611425a](https://github.com/bloomedai/omaship/commit/611425a8d84446312daa33afb0c5ed49e8500e01))
15
+ * 🐛 HTML-escape landing page inputs, prompt injection defense, regenerate on rename ([#353](https://github.com/bloomedai/omaship/issues/353)) ([85822df](https://github.com/bloomedai/omaship/commit/85822dfdc4dba3758ff506b4e08422d9c8be3187))
16
+ * 🐛 move cli ship identity into api ([c331548](https://github.com/bloomedai/omaship/commit/c331548e8953de09524aa780b550bf2100cab502))
17
+ * 🐛 remove cli skip-purpose flag ([b815ff7](https://github.com/bloomedai/omaship/commit/b815ff767430a5301012a1ba44583e305446f1a8))
18
+ * 🐛 support landing ships in cli ([1a2f7fe](https://github.com/bloomedai/omaship/commit/1a2f7fecfa1f1b1ad277e6e007dd7303a1d2420a))
19
+
20
+
21
+ ### Code Refactoring
22
+
23
+ * ♻️ rename LandingPage to Landingpage ([#337](https://github.com/bloomedai/omaship/issues/337)) ([16e9467](https://github.com/bloomedai/omaship/commit/16e9467dceea8e9a595f4be2edf8b89e9759bdfa))
24
+
25
+ ## [0.4.0](https://github.com/bloomedai/omaship/compare/omaship/v0.3.0...omaship/v0.4.0) (2026-03-14)
26
+
27
+
28
+ ### Features
29
+
30
+ * ✨ redesign onboarding flow with progressive CLI terminal path ([#324](https://github.com/bloomedai/omaship/issues/324)) ([872b145](https://github.com/bloomedai/omaship/commit/872b145a2f72775448375557246b4b31fe194459))
31
+
32
+ ## [0.3.0](https://github.com/bloomedai/omaship/compare/omaship/v0.2.2...omaship/v0.3.0) (2026-03-09)
33
+
34
+
35
+ ### Features
36
+
37
+ * ✨ ship the free landing-page flow end to end ([#290](https://github.com/bloomedai/omaship/issues/290)) ([352c088](https://github.com/bloomedai/omaship/commit/352c088219acff3a24a72cda965d12bf1767af1e))
38
+
3
39
  ## [0.2.2](https://github.com/bloomedai/omaship/compare/omaship/v0.2.1...omaship/v0.2.2) (2026-03-06)
4
40
 
5
41
 
data/README.md CHANGED
@@ -18,7 +18,7 @@ brew install omaship
18
18
  ## Quick Start
19
19
 
20
20
  ```bash
21
- omaship login --token <token> --host https://build.omaship.com
21
+ omaship login --token <token>
22
22
  omaship list
23
23
  omaship use <ship-ref>
24
24
  omaship info
@@ -33,7 +33,7 @@ omaship use 17
33
33
 
34
34
  ## Ship Selection
35
35
 
36
- `info`, `configure`, and `deploy` resolve the target ship in this order:
36
+ `info` and `deploy` resolve the target ship in this order:
37
37
 
38
38
  1. `--ship <ship-ref>`
39
39
  2. saved default from `omaship use <ship-ref>`
@@ -51,14 +51,26 @@ Numeric ids are also accepted.
51
51
  - `omaship use <ship-ref>`
52
52
  - `omaship info [--ship <ship-ref>]` (`status` and `ship` are aliases)
53
53
  - `omaship new <name>` (requires Full CLI access)
54
- - `omaship configure --payments <provider> [--ship <ship-ref>]` (requires Full CLI access)
55
54
  - `omaship deploy [--ship <ship-ref>]` (requires Full CLI access)
56
55
  - `omaship complete <bash|zsh|fish>` (print shell completion script)
57
56
  - `omaship logout`
58
57
 
59
- ## Local Development Provisioning
58
+ ## Product Direction
60
59
 
61
- When you run `omaship new <name>` against local Omaship development (`OMASHIP_HOST=http://localhost:3000`), provisioning first installs gems from `https://packages.omaship.com`.
60
+ Omaship CLI is evolving to support two complementary use cases:
61
+
62
+ - standalone Omaship users launching and operating ship runs directly
63
+ - Sokrates-orchestrated workflows where Sokrates calls Omaship capabilities
64
+
65
+ Related docs:
66
+
67
+ - [Sokrates × Omaship Vision](../docs/sokrates-omaship-vision.md)
68
+ - [Sokrates Integration Task List](../docs/sokrates-integration-tasklist.md)
69
+ - [Landing Pages Improvement Plan](../docs/landingpages-improvement-plan.md)
70
+
71
+ ## Local Development Run Execution
72
+
73
+ When you run `omaship new <name>` against local Omaship development (`OMASHIP_HOST=http://localhost:3000`), the initial run first installs gems from `https://packages.omaship.com`.
62
74
 
63
75
  If that registry endpoint is unavailable, Omaship automatically retries `bundle install` through the local package proxy mirror at `http://build.localhost:3000/packages`.
64
76
 
@@ -94,7 +106,7 @@ omaship complete fish > ~/.config/fish/completions/omaship.fish
94
106
 
95
107
  ## Environment
96
108
 
97
- - `OMASHIP_HOST` (default: `http://localhost:3000`)
109
+ - `OMASHIP_HOST` (default: `https://omaship.com`)
98
110
  - `OMASHIP_TOKEN` (optional alternative for `login`)
99
111
 
100
112
  ## Credentials
@@ -108,14 +120,14 @@ omaship complete fish > ~/.config/fish/completions/omaship.fish
108
120
  Run from repository root:
109
121
 
110
122
  ```bash
111
- mise exec ruby@4.0.1 -- env BUNDLE_GEMFILE=cli/Gemfile bundle exec ruby cli/bin/omaship -h
123
+ mise exec ruby@4.0.2 -- env BUNDLE_GEMFILE=cli/Gemfile bundle exec ruby cli/bin/omaship -h
112
124
  ```
113
125
 
114
126
  Run all CLI tests:
115
127
 
116
128
  ```bash
117
129
  cd cli
118
- mise exec ruby@4.0.1 -- bundle exec ruby -Itest -e 'Dir.glob("test/**/*_test.rb").sort.each { |f| require "./#{f}" }'
130
+ mise exec ruby@4.0.2 -- bundle exec ruby -Itest -e 'Dir.glob("test/**/*_test.rb").sort.each { |f| require "./#{f}" }'
119
131
  ```
120
132
 
121
133
  ## Release Automation
@@ -8,6 +8,7 @@ module Omaship
8
8
  class UnauthorizedError < Error; end
9
9
  class PermissionDeniedError < Error; end
10
10
  class NotFoundError < Error; end
11
+ class UpgradeRequiredError < Error; end
11
12
 
12
13
  def initialize(host:, token:)
13
14
  @host = host.to_s
@@ -26,16 +27,15 @@ module Omaship
26
27
  post_json("/api/v1/cli/ships", { ship: { root_domain: root_domain, visibility: visibility } })
27
28
  end
28
29
 
29
- def ship(ship_id:)
30
- get_json("/api/v1/cli/ships/#{ship_id}")
31
- end
32
-
33
- def ship_logs(ship_id:)
34
- get_json("/api/v1/cli/ships/#{ship_id}/logs")
30
+ def create_landingpage(name:, color_scheme: "mono-dark", purpose_profile: {})
31
+ post_json("/api/v1/cli/ships", {
32
+ ship: { name: name, landingpage: true, color_scheme: color_scheme },
33
+ purpose_profile: purpose_profile
34
+ })
35
35
  end
36
36
 
37
- def create_configuration(ship_id:, package:)
38
- post_json("/api/v1/cli/ships/#{ship_id}/configurations", { configuration: { package: package } })
37
+ def ship(ship_id:)
38
+ get_json("/api/v1/cli/ships/#{ship_id}")
39
39
  end
40
40
 
41
41
  def create_deploy(ship_id:)
@@ -61,11 +61,9 @@ module Omaship
61
61
  def parse_response(response)
62
62
  case response.status
63
63
  when 200..299
64
- if response.body.to_s.empty?
65
- {}
66
- else
67
- JSON.parse(response.body)
68
- end
64
+ response_json(response)
65
+ when 426
66
+ raise upgrade_required_error(response)
69
67
  when 401
70
68
  raise UnauthorizedError, "Unauthorized"
71
69
  when 403
@@ -81,9 +79,55 @@ module Omaship
81
79
  @connection ||= Faraday.new(url: @host) do |faraday|
82
80
  faraday.request :retry, max: 2, interval: 0.1
83
81
  faraday.adapter Faraday.default_adapter
84
- faraday.headers["Authorization"] = "Bearer #{@token}"
85
- faraday.headers["Accept"] = "application/json"
82
+ faraday.headers.update(request_headers)
86
83
  end
87
84
  end
85
+
86
+ def request_headers
87
+ {
88
+ "Authorization" => "Bearer #{@token}",
89
+ "Accept" => "application/json",
90
+ "X-Omaship-CLI-Version" => Omaship::VERSION
91
+ }
92
+ end
93
+
94
+ def response_json(response)
95
+ if response.body.to_s.empty?
96
+ {}
97
+ else
98
+ JSON.parse(response.body)
99
+ end
100
+ rescue JSON::ParserError
101
+ {}
102
+ end
103
+
104
+ def upgrade_required_error(response)
105
+ payload = response_json(response)
106
+ minimum_version = payload["minimum_version"].to_s.strip
107
+ current_version = payload["current_version"].to_s.strip
108
+ upgrade_command = payload["upgrade_command"].to_s.strip
109
+
110
+ message = []
111
+
112
+ if current_version.empty?
113
+ message << "Your CLI is no longer supported."
114
+ else
115
+ message << "CLI #{current_version} is no longer supported."
116
+ end
117
+
118
+ if minimum_version.empty?
119
+ message << "Install the latest version."
120
+ else
121
+ message << "Minimum supported version is #{minimum_version}."
122
+ end
123
+
124
+ if upgrade_command.empty?
125
+ message << "Upgrade your CLI and try again."
126
+ else
127
+ message << "Run `#{upgrade_command}` and try again."
128
+ end
129
+
130
+ UpgradeRequiredError.new(message.join(" "))
131
+ end
88
132
  end
89
133
  end
data/lib/omaship/cli.rb CHANGED
@@ -51,8 +51,8 @@ module Omaship
51
51
  shell.say " use Set default ship from `omaship list` (for example: `omaship use omaship/acme`)"
52
52
  shell.say " info Show details for a ship (aliases: ship, status)"
53
53
  shell.say " new Create and provision a new ship"
54
- shell.say " configure Configure a ship (requires Full CLI access)"
55
54
  shell.say " deploy Deploy a ship (requires Full CLI access)"
55
+ shell.say " upgrade Open browser to upgrade your plan"
56
56
  shell.say " complete Print shell completion script (bash, zsh, fish)"
57
57
  shell.say " logout Remove local CLI credentials"
58
58
  shell.say
@@ -143,8 +143,7 @@ module Omaship
143
143
  say "Ships:"
144
144
  ships.each do |ship|
145
145
  marker = default_ship_match?(ship: ship, ship_reference: default_ship_reference) ? "*" : " "
146
- app_url = ship["app_url"] || "-"
147
- say "#{marker} #{ship.fetch("id")} #{ship.fetch("full_name")} #{ship.fetch("status")} #{app_url}"
146
+ say "#{marker} #{ship.fetch("id")} #{ship.fetch("display_name")} #{ship.fetch("status")} #{ship.fetch("primary_url", "-") || "-"}"
148
147
  end
149
148
 
150
149
  if !default_ship_reference.empty?
@@ -161,6 +160,7 @@ module Omaship
161
160
 
162
161
  Get available values with `omaship list`.
163
162
  Preferred format is full ship name (`org/repo`), for example `omaship/acme`.
163
+ Landing pages can also be selected by root domain, for example `acme.omaship.app`.
164
164
  Numeric ship ids from `omaship list` also work.
165
165
 
166
166
  Examples:
@@ -173,33 +173,38 @@ module Omaship
173
173
  ship = find_ship_by_reference(token: token, ship_reference: ship_reference)
174
174
  persist_default_ship(ship)
175
175
 
176
- say "Default ship set to #{ship.fetch("full_name")}."
176
+ say "Default ship set to #{ship.fetch("reference")}."
177
177
  end
178
178
  end
179
179
 
180
180
  desc "info", "Show ship info"
181
- method_option :ship, type: :string, desc: "Ship from `omaship list` (preferred: omaship/acme; id also works)"
181
+ method_option :ship, type: :string, desc: "Ship from `omaship list` (preferred: omaship/acme or acme.omaship.app; id also works)"
182
182
  def info
183
183
  with_api_error_handling(command: :info) do
184
184
  token = current_token
185
185
  client = build_api_client(token: token)
186
186
  resolved_ship = resolve_ship(token: token)
187
187
  ship = client.ship(ship_id: resolved_ship.fetch("id")).fetch("ship")
188
- deploy = client.latest_deploy(ship_id: ship.fetch("id")).fetch("deploy")
189
188
  default_ship_reference = credentials.default_ship.to_s.strip
190
189
 
191
- say "Ship: #{ship.fetch("full_name")}"
190
+ say "Ship: #{ship.fetch("display_name")}"
192
191
  say "ID: #{ship.fetch("id")}"
193
192
  say "Status: #{ship.fetch("status")}"
194
193
  say "App URL: #{ship["app_url"] || "-"}"
194
+ say "Landing URL: #{ship["landing_url"] || "-"}"
195
195
  say "Repo URL: #{ship["repo_url"] || "-"}"
196
196
  say "Default Ship: #{default_ship_reference.empty? ? "-" : default_ship_reference}"
197
- say "Last Deploy: #{deploy_status_summary(deploy)}"
198
- if deploy["run_number"]
199
- say "Run: ##{deploy.fetch("run_number")}"
200
- end
201
- if deploy["started_at"]
202
- say "Started At: #{deploy.fetch("started_at")}"
197
+
198
+ if ship.fetch("deployable")
199
+ deploy = client.latest_deploy(ship_id: ship.fetch("id")).fetch("deploy")
200
+
201
+ say "Last Deploy: #{deploy_status_summary(deploy)}"
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")}"
207
+ end
203
208
  end
204
209
  end
205
210
  end
@@ -209,42 +214,19 @@ module Omaship
209
214
  def new_ship(name)
210
215
  with_api_error_handling(command: :new_ship) do
211
216
  token = current_token
212
- root_domain = options[:domain] || "#{name}.com"
213
- payload = build_api_client(token: token).create_ship(root_domain: root_domain)
214
- ship = payload.fetch("ship")
215
-
216
- progress_renderer.step("Setting up your codebase")
217
- final_ship = poll_until_terminal(ship_id: ship.fetch("id"), token: token)
217
+ auth = fetch_auth(token: token)
218
+ plan = auth.dig("user", "plan") || "free"
218
219
 
219
- if final_ship.fetch("status") == "live"
220
- progress_renderer.step("Ready. Customers can sign up and pay at #{final_ship.fetch("root_domain")}")
220
+ if plan == "free"
221
+ create_free_ship(name: name, token: token)
221
222
  else
222
- message = final_ship["error_message"] || "Provisioning failed"
223
- raise Thor::Error, message
223
+ create_paid_ship(name: name, token: token)
224
224
  end
225
225
  end
226
226
  end
227
227
 
228
- desc "configure --payments PROVIDER", "Configure a ship"
229
- method_option :payments, type: :string, required: true
230
- method_option :ship, type: :string, desc: "Ship from `omaship list` (preferred: omaship/acme; id also works)"
231
- def configure
232
- with_api_error_handling(command: :configure) do
233
- payments_provider = options[:payments].to_s.strip.downcase
234
-
235
- token = current_token
236
- ship = resolve_ship(token: token)
237
- build_api_client(token: token).create_configuration(ship_id: ship.fetch("id"), package: payments_provider)
238
-
239
- progress_renderer.step("Configuration queued.")
240
-
241
- poll_until_configuration_complete(ship_id: ship.fetch("id"), token: token)
242
- progress_renderer.step("Configuration applied.")
243
- end
244
- end
245
-
246
228
  desc "deploy", "Deploy a ship (requires Full CLI access)"
247
- method_option :ship, type: :string, desc: "Ship from `omaship list` (preferred: omaship/acme; id also works)"
229
+ method_option :ship, type: :string, desc: "Ship from `omaship list` (preferred: omaship/acme or acme.omaship.app; id also works)"
248
230
  def deploy
249
231
  with_api_error_handling(command: :deploy) do
250
232
  token = current_token
@@ -260,6 +242,17 @@ module Omaship
260
242
  end
261
243
  end
262
244
 
245
+ desc "upgrade [SHIP]", "Open browser to upgrade your plan"
246
+ def upgrade(ship_name = nil)
247
+ url = "#{resolved_host}/settings/license"
248
+ if ship_name
249
+ url = "#{url}?ship=#{ship_name}"
250
+ end
251
+
252
+ say "Opening #{url}"
253
+ open_browser(url)
254
+ end
255
+
263
256
  desc "complete SHELL", "Print shell completion script (bash, zsh, fish)"
264
257
  long_desc <<~DESC
265
258
  Print a shell completion script to stdout.
@@ -307,7 +300,7 @@ module Omaship
307
300
  fi
308
301
 
309
302
  if [[ ${COMP_CWORD} -eq 1 ]]; then
310
- COMPREPLY=( $(compgen -W "login whoami list use info status ship new configure deploy logout complete help" -- "${cur}") )
303
+ COMPREPLY=( $(compgen -W "login whoami list use info status ship new deploy upgrade logout complete help" -- "${cur}") )
311
304
  return 0
312
305
  fi
313
306
 
@@ -316,11 +309,6 @@ module Omaship
316
309
  return 0
317
310
  fi
318
311
 
319
- if [[ "${prev}" == "--payments" ]]; then
320
- COMPREPLY=( $(compgen -W "stripe" -- "${cur}") )
321
- return 0
322
- fi
323
-
324
312
  if [[ "${prev}" == "--ship" ]]; then
325
313
  COMPREPLY=( $(compgen -W "$(omaship list 2>/dev/null | awk '/^[* ] [0-9]+ / { print $3 }')" -- "${cur}") )
326
314
  return 0
@@ -347,9 +335,6 @@ module Omaship
347
335
  new)
348
336
  COMPREPLY=( $(compgen -W "--domain --host -h --help" -- "${cur}") )
349
337
  ;;
350
- configure)
351
- COMPREPLY=( $(compgen -W "--payments --ship --host -h --help" -- "${cur}") )
352
- ;;
353
338
  complete)
354
339
  COMPREPLY=( $(compgen -W "bash zsh fish" -- "${cur}") )
355
340
  ;;
@@ -386,8 +371,8 @@ module Omaship
386
371
  'status:Alias for info'
387
372
  'ship:Alias for info'
388
373
  'new:Create and provision a new ship'
389
- 'configure:Configure a ship'
390
374
  'deploy:Deploy a ship'
375
+ 'upgrade:Open browser to upgrade your plan'
391
376
  'logout:Remove local CLI credentials'
392
377
  'complete:Print shell completion script'
393
378
  'help:Describe available commands'
@@ -419,9 +404,6 @@ module Omaship
419
404
  new)
420
405
  _arguments '--domain[Root domain]:domain:' '--host[API host]:host:' '1:name:'
421
406
  ;;
422
- configure)
423
- _arguments '--payments[Payments provider]:provider:(stripe)' '--ship[Ship from omaship list]:ship:_omaship_ship_refs' '--host[API host]:host:'
424
- ;;
425
407
  complete)
426
408
  _arguments '1:shell:(bash zsh fish)'
427
409
  ;;
@@ -444,19 +426,145 @@ module Omaship
444
426
  end
445
427
 
446
428
  complete -c omaship -f
447
- complete -c omaship -n '__fish_use_subcommand' -a 'login whoami list use info status ship new configure deploy logout complete help'
429
+ complete -c omaship -n '__fish_use_subcommand' -a 'login whoami list use info status ship new deploy upgrade logout complete help'
448
430
  complete -c omaship -l host -d 'API host'
449
431
 
450
432
  complete -c omaship -n '__fish_seen_subcommand_from login' -l token -d 'API token from omaship settings'
451
433
  complete -c omaship -n '__fish_seen_subcommand_from new' -l domain -d 'Root domain'
452
- complete -c omaship -n '__fish_seen_subcommand_from configure' -l payments -d 'Payments provider' -a 'stripe'
453
- complete -c omaship -n '__fish_seen_subcommand_from configure' -l ship -d 'Ship from omaship list' -a '(__fish_omaship_ship_refs)'
454
434
  complete -c omaship -n '__fish_seen_subcommand_from info status ship deploy' -l ship -d 'Ship from omaship list' -a '(__fish_omaship_ship_refs)'
455
435
  complete -c omaship -n '__fish_seen_subcommand_from use' -f -a '(__fish_omaship_ship_refs)'
456
436
  complete -c omaship -n '__fish_seen_subcommand_from complete' -f -a 'bash zsh fish'
457
437
  FISH
458
438
  end
459
439
 
440
+ def fetch_auth(token:)
441
+ build_api_client(token: token).authenticate
442
+ end
443
+
444
+ def create_paid_ship(name:, token:)
445
+ root_domain = options[:domain] || "#{name}.com"
446
+ payload = build_api_client(token: token).create_ship(root_domain: root_domain)
447
+ ship = payload.fetch("ship")
448
+
449
+ progress_renderer.step("Setting up your codebase")
450
+ final_ship = poll_until_terminal(ship_id: ship.fetch("id"), token: token)
451
+
452
+ if final_ship.fetch("status") == "live"
453
+ progress_renderer.step("Ready. Customers can sign up and pay at #{final_ship.fetch("root_domain")}")
454
+ else
455
+ message = final_ship["error_message"] || "Provisioning failed"
456
+ raise Thor::Error, message
457
+ end
458
+ end
459
+
460
+ def create_free_ship(name:, token:)
461
+ print_free_banner
462
+
463
+ purpose_profile = {}
464
+ color_scheme = "mono-dark"
465
+
466
+ if ask_purpose_profile?
467
+ purpose_profile = collect_purpose_profile
468
+ end
469
+ color_scheme = pick_color_scheme
470
+
471
+ say
472
+ progress_renderer.step("Creating your landing page...")
473
+
474
+ payload = build_api_client(token: token).create_landingpage(
475
+ name: name,
476
+ color_scheme: color_scheme,
477
+ purpose_profile: purpose_profile
478
+ )
479
+ ship = payload.fetch("ship")
480
+
481
+ if ship["distillation_status"] == "pending"
482
+ progress_renderer.step("Sokrates is distilling your answers...")
483
+ poll_until_distilled(ship_id: ship.fetch("id"), token: token)
484
+ end
485
+
486
+ say
487
+ say "Live at: https://#{ship.fetch("root_domain")}"
488
+ say "Review and refine at: #{resolved_host}/ships/#{ship.fetch("id")}/purpose_profile"
489
+ end
490
+
491
+ def print_free_banner
492
+ lines = [
493
+ "FREE PLAN",
494
+ "",
495
+ "You'll get a static landing page with a waitlist",
496
+ "on *.omaship.app",
497
+ "",
498
+ "Want the full Rails app on your own server?",
499
+ "Run: omaship upgrade"
500
+ ]
501
+ width = lines.map(&:length).max + 4
502
+ bar = "+#{"-" * (width + 2)}+"
503
+
504
+ say
505
+ say bar
506
+ lines.each do |line|
507
+ say "| #{line.ljust(width)}|"
508
+ end
509
+ say bar
510
+ say
511
+ end
512
+
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
+ def collect_purpose_profile
522
+ say
523
+ say "--- Purpose Profile ---"
524
+ say
525
+
526
+ raw_problem = ask("What problem does your product solve?\n>")
527
+ raw_audience = ask("Who is most affected by this problem?\n>")
528
+ raw_belief = ask("Why is this worth solving? What's your core belief?\n>")
529
+ raw_contribution = ask("What's your contribution to the solution?\n>")
530
+ raw_outcome = ask("What does the world look like when you succeed?\n>")
531
+
532
+ {
533
+ raw_problem: raw_problem,
534
+ raw_audience: raw_audience,
535
+ raw_belief: raw_belief,
536
+ raw_contribution: raw_contribution,
537
+ raw_outcome: raw_outcome
538
+ }.reject { |_k, v| v.to_s.strip.empty? }
539
+ end
540
+
541
+ def pick_color_scheme
542
+ say
543
+ say "--- Color Scheme ---"
544
+ selected = color_picker.pick
545
+ say "Selected: #{selected}"
546
+ selected
547
+ end
548
+
549
+ def color_picker
550
+ @color_picker ||= Omaship::ColorPicker.new
551
+ end
552
+
553
+ def poll_until_distilled(ship_id:, token:)
554
+ client = build_api_client(token: token)
555
+ 30.times do
556
+ ship_payload = client.ship(ship_id: ship_id).fetch("ship")
557
+ status = ship_payload["distillation_status"]
558
+
559
+ if status == "complete" || status == "skipped" || status == "none"
560
+ return ship_payload
561
+ end
562
+
563
+ sleep 2
564
+ end
565
+ say "Distillation timed out. You can refine later in the dashboard."
566
+ end
567
+
460
568
  def current_token
461
569
  value = credentials.token
462
570
  if value.to_s.strip.empty?
@@ -507,7 +615,7 @@ module Omaship
507
615
  if ship
508
616
  ship
509
617
  else
510
- raise Thor::Error, "Unknown ship `#{ship_reference}`. Run `omaship list` and use the full ship name (for example: `omaship use omaship/acme`) or the numeric id."
618
+ raise Thor::Error, "Unknown ship `#{ship_reference}`. Run `omaship list` and use a ship name or root domain from the list, or the numeric id."
511
619
  end
512
620
  end
513
621
 
@@ -518,11 +626,11 @@ module Omaship
518
626
  end
519
627
 
520
628
  def default_ship_match?(ship:, ship_reference:)
521
- ship.fetch("id").to_s == ship_reference || ship.fetch("full_name") == ship_reference
629
+ ship.fetch("id").to_s == ship_reference || ship.fetch("reference") == ship_reference
522
630
  end
523
631
 
524
632
  def persist_default_ship(ship)
525
- credentials.write_default_ship(ship.fetch("full_name"))
633
+ credentials.write_default_ship(ship.fetch("reference"))
526
634
  end
527
635
 
528
636
  def resolve_ship_for_deploy(token:)
@@ -544,7 +652,7 @@ module Omaship
544
652
  end
545
653
 
546
654
  def multiple_ships_message
547
- "Multiple ships found. Run `omaship list` and `omaship use <org/repo>` (or a ship id)."
655
+ "Multiple ships found. Run `omaship list` and `omaship use <ship-from-list>` (or a ship id)."
548
656
  end
549
657
 
550
658
  def with_api_error_handling(command:)
@@ -558,10 +666,10 @@ module Omaship
558
666
  end
559
667
 
560
668
  def permission_denied_message(command:)
561
- if command == :new_ship
669
+ if current_access_level == "onboarding"
670
+ "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
+ elsif command == :new_ship
562
672
  "`omaship new` requires a full-access token. Create a token with Full CLI access and run `omaship login` again."
563
- elsif command == :configure
564
- "`omaship configure` requires a full-access token. Create a token with Full CLI access and run `omaship login` again."
565
673
  elsif command == :deploy
566
674
  "`omaship deploy` requires a full-access token. Create a token with Full CLI access and run `omaship login` again."
567
675
  else
@@ -569,6 +677,15 @@ module Omaship
569
677
  end
570
678
  end
571
679
 
680
+ def current_access_level
681
+ token = credentials.token
682
+ return "unknown" if token.to_s.strip.empty?
683
+
684
+ build_api_client(token: token).authenticate.dig("token", "access_level").to_s
685
+ rescue NoMethodError, Omaship::ApiClient::Error
686
+ "unknown"
687
+ end
688
+
572
689
  def build_api_client(token:)
573
690
  Omaship::ApiClient.new(host: resolved_host, token: token)
574
691
  end
@@ -581,31 +698,10 @@ module Omaship
581
698
  @progress_renderer ||= Omaship::ProgressRenderer.new(out: $stdout)
582
699
  end
583
700
 
584
- def emit_logs(log_entries)
585
- log_entries.each do |entry|
586
- next unless displayable_log_entry?(entry)
587
-
588
- message = entry.fetch("message")
589
- progress_renderer.step(message)
590
- end
591
- end
592
-
593
- def displayable_log_entry?(entry)
594
- entry.fetch("level", "").to_s != "debug"
595
- end
596
-
597
701
  def poll_until_terminal(ship_id:, token:)
598
702
  client = build_api_client(token: token)
599
- last_log_id = 0
600
703
  90.times do
601
704
  ship_payload = client.ship(ship_id: ship_id).fetch("ship")
602
- logs_payload = client.ship_logs(ship_id: ship_id)
603
- logs = logs_payload.fetch("logs").select { |entry| entry.fetch("id") > last_log_id }
604
- if logs.any?
605
- last_log_id = logs.last.fetch("id")
606
- end
607
-
608
- emit_logs(logs)
609
705
  status = ship_payload.fetch("status")
610
706
  if %w[live error].include?(status)
611
707
  return ship_payload
@@ -616,27 +712,6 @@ module Omaship
616
712
  raise Thor::Error, "Provisioning timed out."
617
713
  end
618
714
 
619
- def poll_until_configuration_complete(ship_id:, token:)
620
- client = build_api_client(token: token)
621
- 60.times do
622
- ship_payload = client.ship(ship_id: ship_id).fetch("ship")
623
- pending = ship_payload.fetch("package_update_pending")
624
- if pending
625
- progress_renderer.step("Applying configuration changes.")
626
- sleep 2
627
- else
628
- error_message = ship_payload["error_message"].to_s.strip
629
- if error_message.empty?
630
- return
631
- end
632
-
633
- raise Thor::Error, error_message
634
- end
635
- end
636
-
637
- raise Thor::Error, "Configuration timed out."
638
- end
639
-
640
715
  def poll_until_deploy_finished(ship_id:, token:)
641
716
  client = build_api_client(token: token)
642
717
  last_status = nil
@@ -686,8 +761,15 @@ module Omaship
686
761
  end
687
762
  end
688
763
 
764
+ def open_browser(url)
765
+ opener = RUBY_PLATFORM.include?("darwin") ? "open" : "xdg-open"
766
+ system(opener, url)
767
+ rescue Errno::ENOENT
768
+ say "Could not open browser. Visit: #{url}"
769
+ end
770
+
689
771
  def resolved_host
690
- options[:host] || credentials.host || ENV["OMASHIP_HOST"] || "http://localhost:3000"
772
+ options[:host] || credentials.host || ENV["OMASHIP_HOST"] || "https://omaship.com"
691
773
  end
692
774
 
693
775
  def existing_credentials_status_message
@@ -0,0 +1,129 @@
1
+ require "io/console"
2
+
3
+ module Omaship
4
+ class ColorPicker
5
+ SCHEMES = [
6
+ { key: "flexoki-light", bg: "#FFFCF0", primary: "#205EA6", accent: "#AD8301", text: "#100F0F" },
7
+ { key: "flexoki-dark", bg: "#100F0F", primary: "#4385BE", accent: "#D0A215", text: "#FFFCF0" },
8
+ { key: "catppuccin-latte", bg: "#EFF1F5", primary: "#8839EF", accent: "#179299", text: "#4C4F69" },
9
+ { key: "catppuccin-mocha", bg: "#1E1E2E", primary: "#CBA6F7", accent: "#94E2D5", text: "#CDD6F4" },
10
+ { key: "rosepine-dawn", bg: "#FAF4ED", primary: "#907AA9", accent: "#D7827E", text: "#575279" },
11
+ { key: "rosepine-moon", bg: "#232136", primary: "#C4A7E7", accent: "#EA9A97", text: "#E0DEF4" },
12
+ { key: "nord-snow", bg: "#ECEFF4", primary: "#5E81AC", accent: "#88C0D0", text: "#2E3440" },
13
+ { key: "nord-frost", bg: "#2E3440", primary: "#88C0D0", accent: "#5E81AC", text: "#ECEFF4" },
14
+ { key: "mono-light", bg: "#FFFFFF", primary: "#18181B", accent: "#71717A", text: "#18181B" },
15
+ { key: "mono-dark", bg: "#18181B", primary: "#FAFAFA", accent: "#A1A1AA", text: "#FAFAFA" },
16
+ { key: "solarized-light", bg: "#FDF6E3", primary: "#268BD2", accent: "#2AA198", text: "#657B83" },
17
+ { key: "solarized-dark", bg: "#002B36", primary: "#268BD2", accent: "#2AA198", text: "#839496" }
18
+ ].freeze
19
+
20
+ DEFAULT_INDEX = 9
21
+
22
+ def initialize(out: $stdout, input: $stdin)
23
+ @out = out
24
+ @input = input
25
+ @index = DEFAULT_INDEX
26
+ end
27
+
28
+ def pick
29
+ render
30
+ loop do
31
+ key = read_key
32
+ case key
33
+ when :left, "h"
34
+ @index = (@index - 1) % SCHEMES.size
35
+ render
36
+ when :right, "l"
37
+ @index = (@index + 1) % SCHEMES.size
38
+ render
39
+ when :enter, "q"
40
+ @out.print "\e[?25h"
41
+ @out.puts
42
+ return SCHEMES[@index][:key]
43
+ when :ctrl_c
44
+ @out.print "\e[?25h"
45
+ @out.puts
46
+ return SCHEMES[DEFAULT_INDEX][:key]
47
+ end
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def render
54
+ scheme = SCHEMES[@index]
55
+ @out.print "\e[?25l"
56
+ @out.print "\r\e[K"
57
+ lines = build_preview(scheme)
58
+ move_up = @rendered_lines || 0
59
+ @out.print "\e[#{move_up}A" if move_up > 0
60
+ lines.each do |line|
61
+ @out.print "\r\e[K#{line}\n"
62
+ end
63
+ @rendered_lines = lines.size
64
+ end
65
+
66
+ def build_preview(scheme)
67
+ 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
80
+
81
+ [
82
+ "",
83
+ " #{left_arrow} #{name} #{right_arrow}",
84
+ "",
85
+ " #{bg_block} bg #{primary_block} primary #{accent_block} accent #{text_block} text",
86
+ "",
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.",
88
+ ""
89
+ ]
90
+ end
91
+
92
+ def left_arrow
93
+ @index > 0 ? "\e[1m<\e[0m" : " "
94
+ end
95
+
96
+ def right_arrow
97
+ @index < SCHEMES.size - 1 ? "\e[1m>\e[0m" : " "
98
+ end
99
+
100
+ def color_block(hex)
101
+ r, g, b = hex_to_rgb(hex)
102
+ "\e[48;2;#{r};#{g};#{b}m \e[0m"
103
+ end
104
+
105
+ def hex_to_rgb(hex)
106
+ hex = hex.delete("#")
107
+ [ hex[0..1], hex[2..3], hex[4..5] ].map { |c| c.to_i(16) }
108
+ 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
+ end
129
+ end
@@ -1,3 +1,3 @@
1
1
  module Omaship
2
- VERSION = "0.2.2".freeze
2
+ VERSION = "0.5.0".freeze
3
3
  end
data/lib/omaship.rb CHANGED
@@ -6,4 +6,5 @@ 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/color_picker"
9
10
  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.2.2
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Omaship
@@ -51,8 +51,7 @@ dependencies:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0'
54
- description: CLI for login, ship provisioning, configuration, and deploy operations
55
- against Omaship.
54
+ description: CLI for login, ship provisioning, and deploy operations against Omaship.
56
55
  email:
57
56
  - hello@omaship.com
58
57
  executables:
@@ -66,6 +65,7 @@ files:
66
65
  - lib/omaship.rb
67
66
  - lib/omaship/api_client.rb
68
67
  - lib/omaship/cli.rb
68
+ - lib/omaship/color_picker.rb
69
69
  - lib/omaship/credentials.rb
70
70
  - lib/omaship/progress_renderer.rb
71
71
  - lib/omaship/ship_detector.rb
@@ -91,7 +91,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
91
91
  - !ruby/object:Gem::Version
92
92
  version: '0'
93
93
  requirements: []
94
- rubygems_version: 4.0.3
94
+ rubygems_version: 4.0.6
95
95
  specification_version: 4
96
96
  summary: Omaship command-line interface
97
97
  test_files: []