aptible-cli 0.13.0 → 0.14.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/README.md +12 -2
- data/aptible-cli.gemspec +4 -3
- data/lib/aptible/cli/agent.rb +9 -2
- data/lib/aptible/cli/helpers/app.rb +19 -0
- data/lib/aptible/cli/helpers/app_or_database.rb +34 -0
- data/lib/aptible/cli/helpers/vhost.rb +83 -0
- data/lib/aptible/cli/helpers/vhost/option_set_builder.rb +292 -0
- data/lib/aptible/cli/subcommands/apps.rb +1 -13
- data/lib/aptible/cli/subcommands/domains.rb +3 -1
- data/lib/aptible/cli/subcommands/endpoints.rb +183 -0
- data/lib/aptible/cli/subcommands/logs.rb +5 -16
- data/lib/aptible/cli/version.rb +1 -1
- data/spec/aptible/cli/helpers/vhost_spec.rb +105 -0
- data/spec/aptible/cli/subcommands/apps_spec.rb +1 -1
- data/spec/aptible/cli/subcommands/endpoints_spec.rb +604 -0
- data/spec/fabricators/account_fabricator.rb +7 -1
- data/spec/fabricators/app_fabricator.rb +5 -0
- data/spec/fabricators/certificate_fabricator.rb +11 -0
- data/spec/fabricators/database_fabricator.rb +10 -1
- data/spec/fabricators/service_fabricator.rb +25 -3
- data/spec/fabricators/vhost_fabricator.rb +5 -2
- metadata +38 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a6a4807415f02ddc44c0472480e3452ff0945d4a
|
4
|
+
data.tar.gz: 7bc74d1f880230acbbec467bdb0c8802a8d3b8cb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c8477ebfa7cf401acdcc3e49d37e06a633489e6cbf44318f34d39bb2050e1e18452c965535964b6bc70b94c9ee9a4bcfe91131bb05c63db87ea32ddce021b309
|
7
|
+
data.tar.gz: 3cc698ae24440386dc5063560eb4af84d295745d043f9888cbc8104a8f6f51470231ceaa26edffb780b5fcbbf8df6c3f22480b6e1497f6d1f9065536fd57bd9f
|
data/README.md
CHANGED
@@ -52,10 +52,20 @@ Commands:
|
|
52
52
|
aptible db:tunnel HANDLE # Create a local tunnel to a database
|
53
53
|
aptible db:url HANDLE # Display a database URL
|
54
54
|
aptible deploy [OPTIONS] [VAR1=VAL1] [VAR=VAL2] ... # Deploy an app
|
55
|
-
aptible domains # Print an app's current virtual domains
|
55
|
+
aptible domains # Print an app's current virtual domains - DEPRECATED
|
56
|
+
aptible endpoints:database:create DATABASE # Create a Database Endpoint
|
57
|
+
aptible endpoints:deprovision [--app APP | --database DATABASE] ENDPOINT_HOSTNAME # Deprovision an App or Database Endpoint
|
58
|
+
aptible endpoints:https:create [--app APP] SERVICE # Create an App HTTPS Endpoint
|
59
|
+
aptible endpoints:https:modify [--app APP] ENDPOINT_HOSTNAME # Modify an App HTTPS Endpoint
|
60
|
+
aptible endpoints:list [--app APP | --database DATABASE] # List Endpoints for an App or Database
|
61
|
+
aptible endpoints:renew [--app APP] ENDPOINT_HOSTNAME # Renew an App Managed TLS Endpoint
|
62
|
+
aptible endpoints:tcp:create [--app APP] SERVICE # Create an App TCP Endpoint
|
63
|
+
aptible endpoints:tcp:modify [--app APP] ENDPOINT_HOSTNAME # Modify an App TCP Endpoint
|
64
|
+
aptible endpoints:tls:create [--app APP] SERVICE # Create an App TLS Endpoint
|
65
|
+
aptible endpoints:tls:modify [--app APP] ENDPOINT_HOSTNAME # Modify an App TLS Endpoint
|
56
66
|
aptible help [COMMAND] # Describe available commands or one specific command
|
57
67
|
aptible login # Log in to Aptible
|
58
|
-
aptible logs
|
68
|
+
aptible logs [--app APP | --database DATABASE] # Follows logs from a running app or database
|
59
69
|
aptible operation:cancel OPERATION_ID # Cancel a running operation
|
60
70
|
aptible ps # Display running processes for an app - DEPRECATED
|
61
71
|
aptible rebuild # Rebuild an app, and restart its services
|
data/aptible-cli.gemspec
CHANGED
@@ -20,9 +20,10 @@ Gem::Specification.new do |spec|
|
|
20
20
|
spec.test_files = spec.files.grep(%r{spec/})
|
21
21
|
spec.require_paths = ['lib']
|
22
22
|
|
23
|
-
spec.add_dependency 'aptible-resource', '~> 0.
|
24
|
-
spec.add_dependency 'aptible-api', '~> 0
|
25
|
-
spec.add_dependency 'aptible-auth', '~> 0
|
23
|
+
spec.add_dependency 'aptible-resource', '~> 1.0', '>= 1.0.1'
|
24
|
+
spec.add_dependency 'aptible-api', '~> 1.0'
|
25
|
+
spec.add_dependency 'aptible-auth', '~> 1.0'
|
26
|
+
spec.add_dependency 'aptible-billing', '~> 1.0'
|
26
27
|
spec.add_dependency 'thor', '~> 0.19.1'
|
27
28
|
spec.add_dependency 'git'
|
28
29
|
spec.add_dependency 'term-ansicolor'
|
data/lib/aptible/cli/agent.rb
CHANGED
@@ -11,6 +11,9 @@ require_relative 'helpers/operation'
|
|
11
11
|
require_relative 'helpers/environment'
|
12
12
|
require_relative 'helpers/app'
|
13
13
|
require_relative 'helpers/database'
|
14
|
+
require_relative 'helpers/app_or_database'
|
15
|
+
require_relative 'helpers/vhost'
|
16
|
+
require_relative 'helpers/vhost/option_set_builder'
|
14
17
|
require_relative 'helpers/tunnel'
|
15
18
|
|
16
19
|
require_relative 'subcommands/apps'
|
@@ -26,6 +29,7 @@ require_relative 'subcommands/ssh'
|
|
26
29
|
require_relative 'subcommands/backup'
|
27
30
|
require_relative 'subcommands/operation'
|
28
31
|
require_relative 'subcommands/inspect'
|
32
|
+
require_relative 'subcommands/endpoints'
|
29
33
|
|
30
34
|
module Aptible
|
31
35
|
module CLI
|
@@ -47,6 +51,7 @@ module Aptible
|
|
47
51
|
include Subcommands::Backup
|
48
52
|
include Subcommands::Operation
|
49
53
|
include Subcommands::Inspect
|
54
|
+
include Subcommands::Endpoints
|
50
55
|
|
51
56
|
# Forward return codes on failures.
|
52
57
|
def self.exit_on_failure?
|
@@ -116,8 +121,10 @@ module Aptible
|
|
116
121
|
private
|
117
122
|
|
118
123
|
def deprecated(msg)
|
119
|
-
|
120
|
-
|
124
|
+
$stderr.puts yellow([
|
125
|
+
"DEPRECATION NOTICE: #{msg}",
|
126
|
+
'Please contact support@aptible.com with any questions.'
|
127
|
+
].join("\n"))
|
121
128
|
end
|
122
129
|
|
123
130
|
def nag_toolbelt
|
@@ -132,6 +132,25 @@ module Aptible
|
|
132
132
|
end
|
133
133
|
end
|
134
134
|
|
135
|
+
def ensure_service(options, type)
|
136
|
+
app = ensure_app(options)
|
137
|
+
service = app.services.find { |s| s.process_type == type }
|
138
|
+
|
139
|
+
if service.nil?
|
140
|
+
valid_types = if app.services.empty?
|
141
|
+
'NONE (deploy the app first)'
|
142
|
+
else
|
143
|
+
app.services.map(&:process_type).join(', ')
|
144
|
+
end
|
145
|
+
|
146
|
+
raise Thor::Error, "Service with type #{type} does not " \
|
147
|
+
"exist for app #{app.handle}. Valid " \
|
148
|
+
"types: #{valid_types}."
|
149
|
+
end
|
150
|
+
|
151
|
+
service
|
152
|
+
end
|
153
|
+
|
135
154
|
def apps_from_handle(handle, environment)
|
136
155
|
if environment
|
137
156
|
environment.apps
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Aptible
|
2
|
+
module CLI
|
3
|
+
module Helpers
|
4
|
+
module AppOrDatabase
|
5
|
+
include Helpers::App
|
6
|
+
include Helpers::Database
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def app_or_database_options
|
10
|
+
app_options
|
11
|
+
option :database
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def ensure_app_or_database(options = {})
|
16
|
+
if options[:app] && options[:database]
|
17
|
+
m = 'You must specify only one of --app and --database'
|
18
|
+
raise Thor::Error, m
|
19
|
+
end
|
20
|
+
|
21
|
+
if options[:database]
|
22
|
+
ensure_database(options.merge(db: options[:database]))
|
23
|
+
else
|
24
|
+
ensure_app(options)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.included(base)
|
29
|
+
base.extend(ClassMethods)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Aptible
|
2
|
+
module CLI
|
3
|
+
module Helpers
|
4
|
+
module Vhost
|
5
|
+
def explain_vhost(service, vhost)
|
6
|
+
say "Service: #{service.process_type}"
|
7
|
+
say "Hostname: #{vhost.external_host}"
|
8
|
+
say "Status: #{vhost.status}"
|
9
|
+
|
10
|
+
case vhost.type
|
11
|
+
when 'tcp', 'tls'
|
12
|
+
ports = if vhost.container_ports.any?
|
13
|
+
vhost.container_ports.map(&:to_s).join(' ')
|
14
|
+
else
|
15
|
+
'all'
|
16
|
+
end
|
17
|
+
say "Type: #{vhost.type}"
|
18
|
+
say "Ports: #{ports}"
|
19
|
+
when 'http', 'http_proxy_protocol'
|
20
|
+
port = vhost.container_port ? vhost.container_port : 'default'
|
21
|
+
say 'Type: https'
|
22
|
+
say "Port: #{port}"
|
23
|
+
end
|
24
|
+
|
25
|
+
say "Internal: #{vhost.internal}"
|
26
|
+
|
27
|
+
ip_whitelist = if vhost.ip_whitelist.any?
|
28
|
+
vhost.ip_whitelist.join(' ')
|
29
|
+
else
|
30
|
+
'all traffic'
|
31
|
+
end
|
32
|
+
say "IP Whitelist: #{ip_whitelist}"
|
33
|
+
|
34
|
+
say "Default Domain Enabled: #{vhost.default}"
|
35
|
+
say "Default Domain: #{vhost.virtual_domain}" if vhost.default
|
36
|
+
|
37
|
+
say "Managed TLS Enabled: #{vhost.acme}"
|
38
|
+
if vhost.acme
|
39
|
+
say "Managed TLS Domain: #{vhost.user_domain}"
|
40
|
+
say 'Managed TLS DNS Challenge Hostname: ' \
|
41
|
+
"#{vhost.acme_dns_challenge_host}"
|
42
|
+
say "Managed TLS Status: #{vhost.acme_status}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def provision_vhost_and_explain(service, vhost)
|
47
|
+
op = vhost.create_operation!(type: 'provision')
|
48
|
+
attach_to_operation_logs(op)
|
49
|
+
explain_vhost(service, vhost.reload)
|
50
|
+
# TODO: Instructions if ACME is enabled?
|
51
|
+
end
|
52
|
+
|
53
|
+
def find_vhost(service_enumerator, hostname)
|
54
|
+
seen = []
|
55
|
+
|
56
|
+
service_enumerator.each do |service|
|
57
|
+
service.each_vhost do |vhost|
|
58
|
+
seen << vhost.external_host
|
59
|
+
return vhost if vhost.external_host == hostname
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
e = "Endpoint with hostname #{hostname} does not exist"
|
64
|
+
e = "#{e} (valid hostnames: #{seen.join(', ')})" if seen.any?
|
65
|
+
raise Thor::Error, e
|
66
|
+
end
|
67
|
+
|
68
|
+
def each_vhost(resource, &block)
|
69
|
+
return enum_for(:each_vhost, resource) unless block_given?
|
70
|
+
|
71
|
+
klass = resource.class
|
72
|
+
if klass == Aptible::Api::App
|
73
|
+
resource.each_service(&block)
|
74
|
+
elsif klass == Aptible::Api::Database
|
75
|
+
[resource.service].each(&block)
|
76
|
+
else
|
77
|
+
raise "Unexpected resource: #{klass}"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,292 @@
|
|
1
|
+
module Aptible
|
2
|
+
module CLI
|
3
|
+
module Helpers
|
4
|
+
module Vhost
|
5
|
+
class OptionSetBuilder
|
6
|
+
FLAGS = %i(
|
7
|
+
environment
|
8
|
+
app
|
9
|
+
database
|
10
|
+
create
|
11
|
+
tls
|
12
|
+
ports
|
13
|
+
port
|
14
|
+
).freeze
|
15
|
+
|
16
|
+
def initialize(&block)
|
17
|
+
FLAGS.each { |f| instance_variable_set("@#{f}", false) }
|
18
|
+
instance_exec(&block) if block
|
19
|
+
end
|
20
|
+
|
21
|
+
def declare_options(thor)
|
22
|
+
thor.instance_exec(self) do |builder|
|
23
|
+
option :environment
|
24
|
+
|
25
|
+
if builder.app?
|
26
|
+
app_options
|
27
|
+
|
28
|
+
if builder.create?
|
29
|
+
option(
|
30
|
+
:default_domain,
|
31
|
+
type: :boolean,
|
32
|
+
desc: 'Enable Default Domain on this Endpoint'
|
33
|
+
)
|
34
|
+
|
35
|
+
option(
|
36
|
+
:internal,
|
37
|
+
type: :boolean,
|
38
|
+
desc: 'Restrict this Endpoint to internal traffic'
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
if builder.ports?
|
43
|
+
option(
|
44
|
+
:ports,
|
45
|
+
type: :array,
|
46
|
+
desc: 'A list of ports to expose on this Endpoint'
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
if builder.port?
|
51
|
+
option(
|
52
|
+
:port,
|
53
|
+
type: :numeric,
|
54
|
+
desc: 'A port to expose on this Endpoint'
|
55
|
+
)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
option(
|
60
|
+
:ip_whitelist,
|
61
|
+
type: :array,
|
62
|
+
desc: 'A list of IPv4 sources (addresses or CIDRs) to ' \
|
63
|
+
'which to restrict traffic to this Endpoint'
|
64
|
+
)
|
65
|
+
|
66
|
+
unless builder.create?
|
67
|
+
# Yes, it has to be a dash...
|
68
|
+
# See: https://github.com/erikhuda/thor/pull/551
|
69
|
+
option(
|
70
|
+
:'no-ip_whitelist',
|
71
|
+
type: :boolean,
|
72
|
+
desc: 'Disable IP Whitelist'
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
if builder.tls?
|
77
|
+
option(
|
78
|
+
:certificate_file,
|
79
|
+
type: :string,
|
80
|
+
desc: 'A file containing a certificate to use on this ' \
|
81
|
+
'Endpoint'
|
82
|
+
)
|
83
|
+
option(
|
84
|
+
:private_key_file,
|
85
|
+
type: :string,
|
86
|
+
desc: 'A file containing a private key to use on this ' \
|
87
|
+
'Endpoint'
|
88
|
+
)
|
89
|
+
|
90
|
+
option(
|
91
|
+
:managed_tls,
|
92
|
+
type: :boolean,
|
93
|
+
desc: 'Enable Managed TLS on this Endpoint'
|
94
|
+
)
|
95
|
+
|
96
|
+
option(
|
97
|
+
:managed_tls_domain,
|
98
|
+
desc: 'A domain to use for Managed TLS'
|
99
|
+
)
|
100
|
+
|
101
|
+
option(
|
102
|
+
:certificate_fingerprint,
|
103
|
+
type: :string,
|
104
|
+
desc: 'The fingerprint of an existing Certificate to use ' \
|
105
|
+
'on this Endpoint'
|
106
|
+
)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def prepare(account, options)
|
112
|
+
options = options.dup # We're going to delete keys here
|
113
|
+
verify_option_conflicts(options)
|
114
|
+
|
115
|
+
params = {}
|
116
|
+
|
117
|
+
params[:ip_whitelist] = options.delete(:ip_whitelist) do
|
118
|
+
create? ? [] : nil
|
119
|
+
end
|
120
|
+
|
121
|
+
if options.delete(:'no-ip_whitelist') { false }
|
122
|
+
params[:ip_whitelist] = []
|
123
|
+
end
|
124
|
+
|
125
|
+
params[:container_port] = options.delete(:port) if port?
|
126
|
+
|
127
|
+
if ports?
|
128
|
+
raw_ports = options.delete(:ports) do
|
129
|
+
create? ? [] : nil
|
130
|
+
end
|
131
|
+
|
132
|
+
if raw_ports
|
133
|
+
params[:container_ports] = raw_ports.map do |p|
|
134
|
+
begin
|
135
|
+
Integer(p)
|
136
|
+
rescue ArgumentError
|
137
|
+
m = "Invalid port: #{p}"
|
138
|
+
raise Thor::Error, m
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
if app?
|
145
|
+
params[:internal] = options.delete(:internal) do
|
146
|
+
create? ? false : nil
|
147
|
+
end
|
148
|
+
|
149
|
+
params[:default] = options.delete(:default_domain) do
|
150
|
+
create? ? false : nil
|
151
|
+
end
|
152
|
+
|
153
|
+
options.delete(:app)
|
154
|
+
else
|
155
|
+
params[:internal] = false
|
156
|
+
end
|
157
|
+
|
158
|
+
process_tls(account, options, params) if tls?
|
159
|
+
|
160
|
+
options.delete(:environment)
|
161
|
+
|
162
|
+
# NOTE: This is here to ensure that specs don't test for options
|
163
|
+
# that are not declared. This is not expected to happen when using
|
164
|
+
# this.
|
165
|
+
raise "Unexpected options: #{options}" if options.any?
|
166
|
+
|
167
|
+
params.delete_if { |_, v| v.nil? }
|
168
|
+
end
|
169
|
+
|
170
|
+
FLAGS.each do |f|
|
171
|
+
define_method("#{f}?") { instance_variable_get("@#{f}") }
|
172
|
+
end
|
173
|
+
|
174
|
+
private
|
175
|
+
|
176
|
+
FLAGS.each do |f|
|
177
|
+
define_method("#{f}!") { instance_variable_set("@#{f}", true) }
|
178
|
+
end
|
179
|
+
|
180
|
+
def process_tls(account, options_in, params_out)
|
181
|
+
# Certificate fingerprint option
|
182
|
+
if (fingerprint = options_in.delete(:certificate_fingerprint))
|
183
|
+
params_out[:certificate] = find_certificate(account, fingerprint)
|
184
|
+
end
|
185
|
+
|
186
|
+
# Ad-hoc certificate option
|
187
|
+
certificate_file = options_in.delete(:certificate_file)
|
188
|
+
private_key_file = options_in.delete(:private_key_file)
|
189
|
+
|
190
|
+
if certificate_file || private_key_file
|
191
|
+
if certificate_file.nil?
|
192
|
+
raise Thor::Error, "Missing #{to_flag(:certificate_file)}"
|
193
|
+
end
|
194
|
+
|
195
|
+
if private_key_file.nil?
|
196
|
+
raise Thor::Error, "Missing #{to_flag(:private_key_file)}"
|
197
|
+
end
|
198
|
+
|
199
|
+
opts = begin
|
200
|
+
{
|
201
|
+
certificate_body: File.read(certificate_file),
|
202
|
+
private_key: File.read(private_key_file)
|
203
|
+
}
|
204
|
+
rescue StandardError => e
|
205
|
+
m = 'Failed to read certificate or private key ' \
|
206
|
+
"file: #{e}"
|
207
|
+
raise Thor::Error, m
|
208
|
+
end
|
209
|
+
|
210
|
+
params_out[:certificate] = account.create_certificate!(opts)
|
211
|
+
end
|
212
|
+
|
213
|
+
# ACME option
|
214
|
+
params_out[:acme] = options_in.delete(:managed_tls) do
|
215
|
+
create? ? false : nil
|
216
|
+
end
|
217
|
+
|
218
|
+
params_out[:user_domain] = options_in.delete(:managed_tls_domain)
|
219
|
+
|
220
|
+
if create? && params_out[:acme] && params_out[:user_domain].nil?
|
221
|
+
e = "#{to_flag(:managed_tls_domain)} is required to enable " \
|
222
|
+
'Managed TLS'
|
223
|
+
raise Thor::Error, e
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def find_certificate(account, fingerprint)
|
228
|
+
matches = []
|
229
|
+
account.each_certificate do |certificate|
|
230
|
+
if certificate.sha256_fingerprint == fingerprint
|
231
|
+
return certificate
|
232
|
+
end
|
233
|
+
|
234
|
+
if certificate.sha256_fingerprint.start_with?(fingerprint)
|
235
|
+
matches << certificate
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
matches = matches.uniq(&:sha256_fingerprint)
|
240
|
+
|
241
|
+
case matches.size
|
242
|
+
when 0
|
243
|
+
e = "No certificate matches fingerprint #{fingerprint}"
|
244
|
+
raise Thor::Error, e
|
245
|
+
when 1
|
246
|
+
return matches.first
|
247
|
+
else
|
248
|
+
e = 'Too many certificates match fingerprint ' \
|
249
|
+
"#{fingerprint}, pass a more specific fingerprint "
|
250
|
+
raise Thor::Error, e
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def verify_option_conflicts(options)
|
255
|
+
conflict_groups = [
|
256
|
+
[
|
257
|
+
%i(certificate_file private_key_file),
|
258
|
+
%i(certificate_fingerprint),
|
259
|
+
%i(managed_tls managed_tls_domain),
|
260
|
+
%i(default_domain)
|
261
|
+
],
|
262
|
+
[
|
263
|
+
%i(no-ip_whitelist),
|
264
|
+
%i(ip_whitelist)
|
265
|
+
]
|
266
|
+
]
|
267
|
+
|
268
|
+
conflict_groups.each do |group|
|
269
|
+
matches = group.map do |g|
|
270
|
+
g.any? { |k| !!options[k] }
|
271
|
+
end
|
272
|
+
|
273
|
+
next unless matches.select { |m| !!m }.size > 1
|
274
|
+
|
275
|
+
selected = group.flatten.select do |o|
|
276
|
+
!!options[o]
|
277
|
+
end
|
278
|
+
|
279
|
+
flags = selected.map { |s| to_flag(s) }
|
280
|
+
e = "Conflicting options provided: #{flags.join(', ')}"
|
281
|
+
raise Thor::Error, e
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
def to_flag(sym)
|
286
|
+
"--#{sym.to_s.tr('_', '-')}"
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|