omaship 0.4.0 → 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 +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +17 -4
- data/lib/omaship/api_client.rb +54 -9
- data/lib/omaship/cli.rb +44 -32
- data/lib/omaship/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6cb3e55ca287cec243c7b73d01ecbd5d37b5830fed0707175e3fce4c92168008
|
|
4
|
+
data.tar.gz: 7f38d4db65faaf778be3af3dd13472793f0d6bc396d82d040a20b39e7c770e13
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0f3aba37dd0ab910fb8132e192c9f14dab1678982723c9805d18cd1a2e47e242f5806ce25c847ec38bdb05fa54105f59eec55888c6ad6d8d633e5fe0a6d71636
|
|
7
|
+
data.tar.gz: 50fc535b1dea46931d96d8e17f894e94af3e6b8a035137aa7b5425f4bdbc8a07df23ec386affff5abc5b3641c2eabf6ea5c140a0c1508c738504607e0d0d734d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
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
|
+
|
|
3
25
|
## [0.4.0](https://github.com/bloomedai/omaship/compare/omaship/v0.3.0...omaship/v0.4.0) (2026-03-14)
|
|
4
26
|
|
|
5
27
|
|
data/README.md
CHANGED
|
@@ -55,9 +55,22 @@ Numeric ids are also accepted.
|
|
|
55
55
|
- `omaship complete <bash|zsh|fish>` (print shell completion script)
|
|
56
56
|
- `omaship logout`
|
|
57
57
|
|
|
58
|
-
##
|
|
58
|
+
## Product Direction
|
|
59
59
|
|
|
60
|
-
|
|
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`.
|
|
61
74
|
|
|
62
75
|
If that registry endpoint is unavailable, Omaship automatically retries `bundle install` through the local package proxy mirror at `http://build.localhost:3000/packages`.
|
|
63
76
|
|
|
@@ -107,14 +120,14 @@ omaship complete fish > ~/.config/fish/completions/omaship.fish
|
|
|
107
120
|
Run from repository root:
|
|
108
121
|
|
|
109
122
|
```bash
|
|
110
|
-
mise exec ruby@4.0.
|
|
123
|
+
mise exec ruby@4.0.2 -- env BUNDLE_GEMFILE=cli/Gemfile bundle exec ruby cli/bin/omaship -h
|
|
111
124
|
```
|
|
112
125
|
|
|
113
126
|
Run all CLI tests:
|
|
114
127
|
|
|
115
128
|
```bash
|
|
116
129
|
cd cli
|
|
117
|
-
mise exec ruby@4.0.
|
|
130
|
+
mise exec ruby@4.0.2 -- bundle exec ruby -Itest -e 'Dir.glob("test/**/*_test.rb").sort.each { |f| require "./#{f}" }'
|
|
118
131
|
```
|
|
119
132
|
|
|
120
133
|
## Release Automation
|
data/lib/omaship/api_client.rb
CHANGED
|
@@ -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,9 +27,9 @@ module Omaship
|
|
|
26
27
|
post_json("/api/v1/cli/ships", { ship: { root_domain: root_domain, visibility: visibility } })
|
|
27
28
|
end
|
|
28
29
|
|
|
29
|
-
def
|
|
30
|
+
def create_landingpage(name:, color_scheme: "mono-dark", purpose_profile: {})
|
|
30
31
|
post_json("/api/v1/cli/ships", {
|
|
31
|
-
ship: { name: name,
|
|
32
|
+
ship: { name: name, landingpage: true, color_scheme: color_scheme },
|
|
32
33
|
purpose_profile: purpose_profile
|
|
33
34
|
})
|
|
34
35
|
end
|
|
@@ -60,11 +61,9 @@ module Omaship
|
|
|
60
61
|
def parse_response(response)
|
|
61
62
|
case response.status
|
|
62
63
|
when 200..299
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
JSON.parse(response.body)
|
|
67
|
-
end
|
|
64
|
+
response_json(response)
|
|
65
|
+
when 426
|
|
66
|
+
raise upgrade_required_error(response)
|
|
68
67
|
when 401
|
|
69
68
|
raise UnauthorizedError, "Unauthorized"
|
|
70
69
|
when 403
|
|
@@ -80,9 +79,55 @@ module Omaship
|
|
|
80
79
|
@connection ||= Faraday.new(url: @host) do |faraday|
|
|
81
80
|
faraday.request :retry, max: 2, interval: 0.1
|
|
82
81
|
faraday.adapter Faraday.default_adapter
|
|
83
|
-
faraday.headers
|
|
84
|
-
faraday.headers["Accept"] = "application/json"
|
|
82
|
+
faraday.headers.update(request_headers)
|
|
85
83
|
end
|
|
86
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
|
|
87
132
|
end
|
|
88
133
|
end
|
data/lib/omaship/cli.rb
CHANGED
|
@@ -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
|
-
|
|
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,44 +173,49 @@ 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("
|
|
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("
|
|
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
|
-
|
|
198
|
-
if
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
|
206
211
|
|
|
207
212
|
desc "new NAME", "Create and provision a new ship"
|
|
208
213
|
method_option :domain, type: :string, desc: "Root domain (defaults to NAME.com)"
|
|
209
|
-
method_option :skip_purpose, type: :boolean, default: false, desc: "Skip purpose profile questions"
|
|
210
214
|
def new_ship(name)
|
|
211
215
|
with_api_error_handling(command: :new_ship) do
|
|
212
216
|
token = current_token
|
|
213
|
-
|
|
217
|
+
auth = fetch_auth(token: token)
|
|
218
|
+
plan = auth.dig("user", "plan") || "free"
|
|
214
219
|
|
|
215
220
|
if plan == "free"
|
|
216
221
|
create_free_ship(name: name, token: token)
|
|
@@ -221,7 +226,7 @@ module Omaship
|
|
|
221
226
|
end
|
|
222
227
|
|
|
223
228
|
desc "deploy", "Deploy a ship (requires Full CLI access)"
|
|
224
|
-
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)"
|
|
225
230
|
def deploy
|
|
226
231
|
with_api_error_handling(command: :deploy) do
|
|
227
232
|
token = current_token
|
|
@@ -328,7 +333,7 @@ module Omaship
|
|
|
328
333
|
COMPREPLY=( $(compgen -W "--ship --host -h --help" -- "${cur}") )
|
|
329
334
|
;;
|
|
330
335
|
new)
|
|
331
|
-
COMPREPLY=( $(compgen -W "--domain --
|
|
336
|
+
COMPREPLY=( $(compgen -W "--domain --host -h --help" -- "${cur}") )
|
|
332
337
|
;;
|
|
333
338
|
complete)
|
|
334
339
|
COMPREPLY=( $(compgen -W "bash zsh fish" -- "${cur}") )
|
|
@@ -397,7 +402,7 @@ module Omaship
|
|
|
397
402
|
_arguments '--ship[Ship from omaship list]:ship:_omaship_ship_refs' '--host[API host]:host:'
|
|
398
403
|
;;
|
|
399
404
|
new)
|
|
400
|
-
_arguments '--domain[Root domain]:domain:' '--
|
|
405
|
+
_arguments '--domain[Root domain]:domain:' '--host[API host]:host:' '1:name:'
|
|
401
406
|
;;
|
|
402
407
|
complete)
|
|
403
408
|
_arguments '1:shell:(bash zsh fish)'
|
|
@@ -426,16 +431,14 @@ module Omaship
|
|
|
426
431
|
|
|
427
432
|
complete -c omaship -n '__fish_seen_subcommand_from login' -l token -d 'API token from omaship settings'
|
|
428
433
|
complete -c omaship -n '__fish_seen_subcommand_from new' -l domain -d 'Root domain'
|
|
429
|
-
complete -c omaship -n '__fish_seen_subcommand_from new' -l skip-purpose -d 'Skip purpose questions'
|
|
430
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)'
|
|
431
435
|
complete -c omaship -n '__fish_seen_subcommand_from use' -f -a '(__fish_omaship_ship_refs)'
|
|
432
436
|
complete -c omaship -n '__fish_seen_subcommand_from complete' -f -a 'bash zsh fish'
|
|
433
437
|
FISH
|
|
434
438
|
end
|
|
435
439
|
|
|
436
|
-
def
|
|
437
|
-
|
|
438
|
-
auth.dig("user", "plan") || "free"
|
|
440
|
+
def fetch_auth(token:)
|
|
441
|
+
build_api_client(token: token).authenticate
|
|
439
442
|
end
|
|
440
443
|
|
|
441
444
|
def create_paid_ship(name:, token:)
|
|
@@ -460,17 +463,15 @@ module Omaship
|
|
|
460
463
|
purpose_profile = {}
|
|
461
464
|
color_scheme = "mono-dark"
|
|
462
465
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
purpose_profile = collect_purpose_profile
|
|
466
|
-
end
|
|
467
|
-
color_scheme = pick_color_scheme
|
|
466
|
+
if ask_purpose_profile?
|
|
467
|
+
purpose_profile = collect_purpose_profile
|
|
468
468
|
end
|
|
469
|
+
color_scheme = pick_color_scheme
|
|
469
470
|
|
|
470
471
|
say
|
|
471
472
|
progress_renderer.step("Creating your landing page...")
|
|
472
473
|
|
|
473
|
-
payload = build_api_client(token: token).
|
|
474
|
+
payload = build_api_client(token: token).create_landingpage(
|
|
474
475
|
name: name,
|
|
475
476
|
color_scheme: color_scheme,
|
|
476
477
|
purpose_profile: purpose_profile
|
|
@@ -614,7 +615,7 @@ module Omaship
|
|
|
614
615
|
if ship
|
|
615
616
|
ship
|
|
616
617
|
else
|
|
617
|
-
raise Thor::Error, "Unknown ship `#{ship_reference}`. Run `omaship list` and use
|
|
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."
|
|
618
619
|
end
|
|
619
620
|
end
|
|
620
621
|
|
|
@@ -625,11 +626,11 @@ module Omaship
|
|
|
625
626
|
end
|
|
626
627
|
|
|
627
628
|
def default_ship_match?(ship:, ship_reference:)
|
|
628
|
-
ship.fetch("id").to_s == ship_reference || ship.fetch("
|
|
629
|
+
ship.fetch("id").to_s == ship_reference || ship.fetch("reference") == ship_reference
|
|
629
630
|
end
|
|
630
631
|
|
|
631
632
|
def persist_default_ship(ship)
|
|
632
|
-
credentials.write_default_ship(ship.fetch("
|
|
633
|
+
credentials.write_default_ship(ship.fetch("reference"))
|
|
633
634
|
end
|
|
634
635
|
|
|
635
636
|
def resolve_ship_for_deploy(token:)
|
|
@@ -651,7 +652,7 @@ module Omaship
|
|
|
651
652
|
end
|
|
652
653
|
|
|
653
654
|
def multiple_ships_message
|
|
654
|
-
"Multiple ships found. Run `omaship list` and `omaship use <
|
|
655
|
+
"Multiple ships found. Run `omaship list` and `omaship use <ship-from-list>` (or a ship id)."
|
|
655
656
|
end
|
|
656
657
|
|
|
657
658
|
def with_api_error_handling(command:)
|
|
@@ -665,7 +666,9 @@ module Omaship
|
|
|
665
666
|
end
|
|
666
667
|
|
|
667
668
|
def permission_denied_message(command:)
|
|
668
|
-
if
|
|
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
|
|
669
672
|
"`omaship new` requires a full-access token. Create a token with Full CLI access and run `omaship login` again."
|
|
670
673
|
elsif command == :deploy
|
|
671
674
|
"`omaship deploy` requires a full-access token. Create a token with Full CLI access and run `omaship login` again."
|
|
@@ -674,6 +677,15 @@ module Omaship
|
|
|
674
677
|
end
|
|
675
678
|
end
|
|
676
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
|
+
|
|
677
689
|
def build_api_client(token:)
|
|
678
690
|
Omaship::ApiClient.new(host: resolved_host, token: token)
|
|
679
691
|
end
|
data/lib/omaship/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: omaship
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Omaship
|
|
@@ -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.
|
|
94
|
+
rubygems_version: 4.0.6
|
|
95
95
|
specification_version: 4
|
|
96
96
|
summary: Omaship command-line interface
|
|
97
97
|
test_files: []
|