aptible-cli 0.13.0 → 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|