chamber 2.13.0 → 3.0.0rc1
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
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/README.md +101 -26
- data/lib/chamber.rb +77 -16
- data/lib/chamber/adapters/cloud/circle_ci.rb +16 -13
- data/lib/chamber/adapters/cloud/heroku.rb +40 -13
- data/lib/chamber/binary/circle_ci.rb +28 -14
- data/lib/chamber/binary/heroku.rb +34 -14
- data/lib/chamber/binary/runner.rb +40 -29
- data/lib/chamber/binary/travis.rb +10 -4
- data/lib/chamber/commands/base.rb +10 -16
- data/lib/chamber/commands/cloud/base.rb +3 -3
- data/lib/chamber/commands/cloud/pull.rb +2 -2
- data/lib/chamber/commands/cloud/push.rb +7 -7
- data/lib/chamber/commands/comparable.rb +2 -2
- data/lib/chamber/commands/compare.rb +6 -9
- data/lib/chamber/commands/initialize.rb +26 -22
- data/lib/chamber/commands/securable.rb +12 -9
- data/lib/chamber/commands/secure.rb +2 -2
- data/lib/chamber/commands/show.rb +8 -8
- data/lib/chamber/commands/sign.rb +2 -2
- data/lib/chamber/commands/verify.rb +2 -2
- data/lib/chamber/configuration.rb +6 -3
- data/lib/chamber/context_resolver.rb +7 -7
- data/lib/chamber/encryption_methods/ssl.rb +12 -12
- data/lib/chamber/file.rb +20 -15
- data/lib/chamber/file_set.rb +18 -8
- data/lib/chamber/files/signature.rb +16 -14
- data/lib/chamber/filters/decryption_filter.rb +19 -15
- data/lib/chamber/filters/encryption_filter.rb +14 -11
- data/lib/chamber/filters/environment_filter.rb +21 -20
- data/lib/chamber/filters/failed_decryption_filter.rb +9 -6
- data/lib/chamber/filters/insecure_filter.rb +4 -5
- data/lib/chamber/filters/namespace_filter.rb +13 -9
- data/lib/chamber/filters/secure_filter.rb +9 -7
- data/lib/chamber/filters/translate_secure_keys_filter.rb +9 -7
- data/lib/chamber/instance.rb +48 -31
- data/lib/chamber/key_pair.rb +7 -7
- data/lib/chamber/keys/base.rb +13 -13
- data/lib/chamber/keys/decryption.rb +3 -3
- data/lib/chamber/keys/encryption.rb +3 -3
- data/lib/chamber/namespace_set.rb +2 -4
- data/lib/chamber/refinements/array.rb +20 -0
- data/lib/chamber/refinements/deep_dup.rb +58 -0
- data/lib/chamber/refinements/enumerable.rb +36 -0
- data/lib/chamber/refinements/hash.rb +51 -0
- data/lib/chamber/settings.rb +81 -66
- data/lib/chamber/types/secured.rb +21 -23
- data/lib/chamber/version.rb +1 -1
- data/templates/settings.yml +2 -0
- metadata +44 -51
- metadata.gz.sig +0 -0
- data/lib/chamber/core_ext/hash.rb +0 -15
@@ -5,11 +5,13 @@ require 'chamber/commands/cloud/clear'
|
|
5
5
|
require 'chamber/commands/cloud/push'
|
6
6
|
require 'chamber/commands/cloud/pull'
|
7
7
|
require 'chamber/commands/cloud/compare'
|
8
|
+
require 'chamber/refinements/hash'
|
8
9
|
|
9
10
|
module Chamber
|
10
11
|
module Binary
|
11
|
-
class Heroku < Thor
|
12
|
-
include Thor::Actions
|
12
|
+
class Heroku < ::Thor
|
13
|
+
include ::Thor::Actions
|
14
|
+
using ::Chamber::Refinements::Hash
|
13
15
|
|
14
16
|
class_option :app,
|
15
17
|
type: :string,
|
@@ -18,8 +20,15 @@ class Heroku < Thor
|
|
18
20
|
desc: 'The name of the Heroku application whose config values will ' \
|
19
21
|
'be affected'
|
20
22
|
|
21
|
-
|
22
|
-
|
23
|
+
class_option :api_token,
|
24
|
+
type: :string,
|
25
|
+
aliases: '-t',
|
26
|
+
required: true,
|
27
|
+
desc: 'The API token to access your Heroku project.'
|
28
|
+
|
29
|
+
desc 'clear',
|
30
|
+
'Removes all Heroku environment variables which match settings that ' \
|
31
|
+
'Chamber knows about'
|
23
32
|
|
24
33
|
method_option :dry_run,
|
25
34
|
type: :boolean,
|
@@ -28,11 +37,14 @@ class Heroku < Thor
|
|
28
37
|
'would change if cleared'
|
29
38
|
|
30
39
|
def clear
|
31
|
-
Commands::Cloud::Clear.call(options
|
40
|
+
Commands::Cloud::Clear.call(**options
|
41
|
+
.deep_transform_keys(&:to_sym)
|
42
|
+
.merge(shell: self, adapter: 'heroku'))
|
32
43
|
end
|
33
44
|
|
34
|
-
desc 'push',
|
35
|
-
|
45
|
+
desc 'push',
|
46
|
+
'Sends settings to Heroku so that they may be used in the application ' \
|
47
|
+
'once it is deployed'
|
36
48
|
|
37
49
|
method_option :dry_run,
|
38
50
|
type: :boolean,
|
@@ -57,11 +69,14 @@ class Heroku < Thor
|
|
57
69
|
'will be pushed'
|
58
70
|
|
59
71
|
def push
|
60
|
-
Commands::Cloud::Push.call(options
|
72
|
+
Commands::Cloud::Push.call(**options
|
73
|
+
.deep_transform_keys(&:to_sym)
|
74
|
+
.merge(shell: self, adapter: 'heroku'))
|
61
75
|
end
|
62
76
|
|
63
|
-
desc 'pull',
|
64
|
-
|
77
|
+
desc 'pull',
|
78
|
+
'Retrieves the environment variables for the application and stores ' \
|
79
|
+
'them in a temporary file'
|
65
80
|
|
66
81
|
method_option :into,
|
67
82
|
type: :string,
|
@@ -69,11 +84,14 @@ class Heroku < Thor
|
|
69
84
|
'stored. This file WILL BE OVERRIDDEN.'
|
70
85
|
|
71
86
|
def pull
|
72
|
-
Commands::Cloud::Pull.call(options
|
87
|
+
Commands::Cloud::Pull.call(**options
|
88
|
+
.deep_transform_keys(&:to_sym)
|
89
|
+
.merge(shell: self, adapter: 'heroku'))
|
73
90
|
end
|
74
91
|
|
75
|
-
desc 'compare',
|
76
|
-
|
92
|
+
desc 'compare',
|
93
|
+
'Displays the difference between what is currently stored in the ' \
|
94
|
+
'Heroku application\'s config and what Chamber knows about locally'
|
77
95
|
|
78
96
|
method_option :only_sensitive,
|
79
97
|
type: :boolean,
|
@@ -84,7 +102,9 @@ class Heroku < Thor
|
|
84
102
|
'which are marked as "_secure"'
|
85
103
|
|
86
104
|
def compare
|
87
|
-
Commands::Cloud::Compare.call(options
|
105
|
+
Commands::Cloud::Compare.call(**options
|
106
|
+
.deep_transform_keys(&:to_sym)
|
107
|
+
.merge(shell: self, adapter: 'heroku'))
|
88
108
|
end
|
89
109
|
end
|
90
110
|
end
|
@@ -12,13 +12,15 @@ require 'chamber/commands/sign'
|
|
12
12
|
require 'chamber/commands/verify'
|
13
13
|
require 'chamber/commands/compare'
|
14
14
|
require 'chamber/commands/initialize'
|
15
|
+
require 'chamber/refinements/hash'
|
15
16
|
|
16
17
|
module Chamber
|
17
18
|
module Binary
|
18
|
-
class Runner < Thor
|
19
|
-
include Thor::Actions
|
19
|
+
class Runner < ::Thor
|
20
|
+
include ::Thor::Actions
|
21
|
+
using ::Chamber::Refinements::Hash
|
20
22
|
|
21
|
-
source_root ::File.expand_path('
|
23
|
+
source_root ::File.expand_path('../../../templates', __dir__)
|
22
24
|
|
23
25
|
class_option :rootpath,
|
24
26
|
type: :string,
|
@@ -62,17 +64,17 @@ class Runner < Thor
|
|
62
64
|
|
63
65
|
################################################################################
|
64
66
|
|
65
|
-
desc 'travis SUBCOMMAND ...ARGS',
|
67
|
+
desc 'travis SUBCOMMAND ...ARGS', 'For manipulating Travis CI environment variables'
|
66
68
|
subcommand 'travis', Chamber::Binary::Travis
|
67
69
|
|
68
70
|
################################################################################
|
69
71
|
|
70
|
-
desc 'heroku SUBCOMMAND ...ARGS',
|
72
|
+
desc 'heroku SUBCOMMAND ...ARGS', 'For manipulating Heroku environment variables'
|
71
73
|
subcommand 'heroku', Chamber::Binary::Heroku
|
72
74
|
|
73
75
|
################################################################################
|
74
76
|
|
75
|
-
desc 'circleci SUBCOMMAND ...ARGS',
|
77
|
+
desc 'circleci SUBCOMMAND ...ARGS', 'For manipulating CircleCI environment variables'
|
76
78
|
subcommand 'circleci', Chamber::Binary::CircleCi
|
77
79
|
|
78
80
|
################################################################################
|
@@ -92,7 +94,7 @@ class Runner < Thor
|
|
92
94
|
'Useful for debugging.'
|
93
95
|
|
94
96
|
def show
|
95
|
-
puts Commands::Show.call(options.merge(shell: self))
|
97
|
+
puts Commands::Show.call(**options.deep_transform_keys(&:to_sym).merge(shell: self))
|
96
98
|
end
|
97
99
|
|
98
100
|
################################################################################
|
@@ -100,15 +102,16 @@ class Runner < Thor
|
|
100
102
|
desc 'files', 'Lists the settings files which are parsed with the given options'
|
101
103
|
|
102
104
|
def files
|
103
|
-
puts Commands::Files.call(options.merge(shell: self))
|
105
|
+
puts Commands::Files.call(**options.deep_transform_keys(&:to_sym).merge(shell: self))
|
104
106
|
end
|
105
107
|
|
106
108
|
################################################################################
|
107
109
|
|
108
|
-
desc 'compare',
|
109
|
-
|
110
|
-
|
111
|
-
|
110
|
+
desc 'compare',
|
111
|
+
'Displays the difference between the settings in the first set ' \
|
112
|
+
'of namespaces and the settings in the second set. Useful for ' \
|
113
|
+
'tracking down why there may be issues in development versus test ' \
|
114
|
+
'or differences between staging and production.'
|
112
115
|
|
113
116
|
method_option :keys_only,
|
114
117
|
type: :boolean,
|
@@ -117,23 +120,26 @@ class Runner < Thor
|
|
117
120
|
'of the two sets of settings'
|
118
121
|
|
119
122
|
method_option :first,
|
120
|
-
type:
|
121
|
-
|
122
|
-
|
123
|
+
type: :array,
|
124
|
+
required: true,
|
125
|
+
desc: 'The list of namespaces which will be used as the source of ' \
|
126
|
+
'the comparison'
|
123
127
|
|
124
128
|
method_option :second,
|
125
|
-
type:
|
126
|
-
|
127
|
-
|
129
|
+
type: :array,
|
130
|
+
required: true,
|
131
|
+
desc: 'The list of namespaces which will be used as the ' \
|
132
|
+
'destination of the comparison'
|
128
133
|
|
129
134
|
def compare
|
130
|
-
Commands::Compare.call(options.merge(shell: self))
|
135
|
+
Commands::Compare.call(**options.deep_transform_keys(&:to_sym).merge(shell: self))
|
131
136
|
end
|
132
137
|
|
133
138
|
################################################################################
|
134
139
|
|
135
|
-
desc 'secure',
|
136
|
-
|
140
|
+
desc 'secure',
|
141
|
+
'Secures any values which appear to need to be encrypted in any of ' \
|
142
|
+
'the settings files which match irrespective of namespaces'
|
137
143
|
|
138
144
|
method_option :only_sensitive,
|
139
145
|
type: :boolean,
|
@@ -146,37 +152,42 @@ class Runner < Thor
|
|
146
152
|
'what values would be encrypted'
|
147
153
|
|
148
154
|
def secure
|
149
|
-
Commands::Secure.call(options.merge(shell: self))
|
155
|
+
Commands::Secure.call(**options.deep_transform_keys(&:to_sym).merge(shell: self))
|
150
156
|
end
|
151
157
|
|
152
158
|
################################################################################
|
153
159
|
|
154
|
-
desc 'sign',
|
155
|
-
|
160
|
+
desc 'sign',
|
161
|
+
'Creates or verifies signatures for all current settings files using ' \
|
162
|
+
'the signature private key.'
|
156
163
|
|
157
164
|
method_option :verify,
|
158
165
|
type: :boolean,
|
159
166
|
default: false
|
160
167
|
|
168
|
+
method_option :signature_name,
|
169
|
+
type: :string,
|
170
|
+
default: `git config --get 'user.name'`.chomp
|
171
|
+
|
161
172
|
def sign
|
162
173
|
if options[:verify]
|
163
|
-
Commands::Verify.call(options.merge(shell: self))
|
174
|
+
Commands::Verify.call(**options.deep_transform_keys(&:to_sym).merge(shell: self))
|
164
175
|
else
|
165
|
-
Commands::Sign.call(options.merge(shell: self))
|
176
|
+
Commands::Sign.call(**options.deep_transform_keys(&:to_sym).merge(shell: self))
|
166
177
|
end
|
167
178
|
end
|
168
179
|
|
169
180
|
################################################################################
|
170
181
|
|
171
|
-
desc 'init',
|
172
|
-
|
182
|
+
desc 'init',
|
183
|
+
'Sets Chamber up using best practices for secure configuration management'
|
173
184
|
|
174
185
|
method_option :signature,
|
175
186
|
type: :boolean,
|
176
187
|
default: false
|
177
188
|
|
178
189
|
def init
|
179
|
-
Commands::Initialize.call(options.merge(shell: self))
|
190
|
+
Commands::Initialize.call(**options.deep_transform_keys(&:to_sym).merge(shell: self))
|
180
191
|
end
|
181
192
|
end
|
182
193
|
end
|
@@ -2,12 +2,16 @@
|
|
2
2
|
|
3
3
|
require 'thor'
|
4
4
|
require 'chamber/commands/travis/secure'
|
5
|
+
require 'chamber/refinements/hash'
|
5
6
|
|
6
7
|
module Chamber
|
7
8
|
module Binary
|
8
|
-
class Travis < Thor
|
9
|
-
|
10
|
-
|
9
|
+
class Travis < ::Thor
|
10
|
+
using ::Chamber::Refinements::Hash
|
11
|
+
|
12
|
+
desc 'secure',
|
13
|
+
'Uses your Travis CI public key to encrypt the settings you have ' \
|
14
|
+
'chosen not to commit to the repo'
|
11
15
|
|
12
16
|
method_option :dry_run,
|
13
17
|
type: :boolean,
|
@@ -24,7 +28,9 @@ class Travis < Thor
|
|
24
28
|
'which are marked as "_secure"'
|
25
29
|
|
26
30
|
def secure
|
27
|
-
Commands::Travis::Secure.call(options
|
31
|
+
Commands::Travis::Secure.call(**options
|
32
|
+
.deep_transform_keys(&:to_sym)
|
33
|
+
.merge(shell: self))
|
28
34
|
end
|
29
35
|
end
|
30
36
|
end
|
@@ -6,26 +6,20 @@ require 'chamber/instance'
|
|
6
6
|
module Chamber
|
7
7
|
module Commands
|
8
8
|
class Base
|
9
|
-
def self.call(
|
10
|
-
new(
|
9
|
+
def self.call(**args)
|
10
|
+
new(**args).call
|
11
11
|
end
|
12
12
|
|
13
13
|
attr_accessor :chamber,
|
14
|
-
:
|
15
|
-
:
|
16
|
-
|
14
|
+
:dry_run,
|
15
|
+
:rootpath,
|
16
|
+
:shell
|
17
17
|
|
18
|
-
def initialize(
|
19
|
-
self.chamber = Chamber::Instance.new
|
20
|
-
self.shell =
|
21
|
-
self.rootpath =
|
22
|
-
self.dry_run =
|
23
|
-
end
|
24
|
-
|
25
|
-
protected
|
26
|
-
|
27
|
-
def rootpath=(other)
|
28
|
-
@rootpath = Pathname.new(other)
|
18
|
+
def initialize(shell: nil, rootpath: nil, dry_run: nil, **args)
|
19
|
+
self.chamber = Chamber::Instance.new(rootpath: rootpath, **args)
|
20
|
+
self.shell = shell
|
21
|
+
self.rootpath = chamber.configuration.rootpath
|
22
|
+
self.dry_run = dry_run
|
29
23
|
end
|
30
24
|
end
|
31
25
|
end
|
@@ -8,10 +8,10 @@ module Cloud
|
|
8
8
|
class Base < Chamber::Commands::Base
|
9
9
|
attr_accessor :adapter
|
10
10
|
|
11
|
-
def initialize(
|
12
|
-
super
|
11
|
+
def initialize(adapter:, **args)
|
12
|
+
super(**args)
|
13
13
|
|
14
|
-
self.adapter = adapter_class(
|
14
|
+
self.adapter = adapter_class(adapter).new(**args)
|
15
15
|
end
|
16
16
|
|
17
17
|
private
|
@@ -9,10 +9,10 @@ module Cloud
|
|
9
9
|
class Pull < Chamber::Commands::Cloud::Base
|
10
10
|
attr_accessor :target_file
|
11
11
|
|
12
|
-
def initialize(
|
12
|
+
def initialize(into:, **args)
|
13
13
|
super
|
14
14
|
|
15
|
-
self.target_file =
|
15
|
+
self.target_file = into
|
16
16
|
end
|
17
17
|
|
18
18
|
def call
|
@@ -12,18 +12,18 @@ class Push < Chamber::Commands::Cloud::Base
|
|
12
12
|
|
13
13
|
attr_accessor :keys
|
14
14
|
|
15
|
-
def initialize(
|
16
|
-
super
|
15
|
+
def initialize(keys:, **args)
|
16
|
+
super(**args)
|
17
17
|
|
18
|
-
self.keys =
|
18
|
+
self.keys = keys
|
19
19
|
end
|
20
20
|
|
21
21
|
def call
|
22
22
|
environment_variables = if keys
|
23
|
-
Keys::Decryption
|
24
|
-
new(rootpath: chamber.configuration.rootpath,
|
25
|
-
|
26
|
-
as_environment_variables
|
23
|
+
Keys::Decryption
|
24
|
+
.new(rootpath: chamber.configuration.rootpath,
|
25
|
+
namespaces: chamber.configuration.namespaces)
|
26
|
+
.as_environment_variables
|
27
27
|
else
|
28
28
|
securable_environment_variables
|
29
29
|
end
|
@@ -12,18 +12,15 @@ class Compare < Chamber::Commands::Base
|
|
12
12
|
attr_accessor :first_settings_instance,
|
13
13
|
:second_settings_instance
|
14
14
|
|
15
|
-
def self.call(
|
16
|
-
new(
|
15
|
+
def self.call(**args)
|
16
|
+
new(**args).call
|
17
17
|
end
|
18
18
|
|
19
|
-
def initialize(
|
20
|
-
super
|
19
|
+
def initialize(first:, second:, **args)
|
20
|
+
super(**args)
|
21
21
|
|
22
|
-
|
23
|
-
self.
|
24
|
-
|
25
|
-
second_settings_options = options.merge(namespaces: options[:second])
|
26
|
-
self.second_settings_instance = Chamber::Instance.new(second_settings_options)
|
22
|
+
self.first_settings_instance = Chamber::Instance.new(args.merge(namespaces: first))
|
23
|
+
self.second_settings_instance = Chamber::Instance.new(args.merge(namespaces: second))
|
27
24
|
end
|
28
25
|
|
29
26
|
protected
|
@@ -11,23 +11,23 @@ require 'chamber/commands/base'
|
|
11
11
|
module Chamber
|
12
12
|
module Commands
|
13
13
|
class Initialize < Chamber::Commands::Base
|
14
|
-
def self.call(
|
15
|
-
new(
|
14
|
+
def self.call(**args)
|
15
|
+
new(**args).call
|
16
16
|
end
|
17
17
|
|
18
18
|
attr_accessor :basepath,
|
19
19
|
:namespaces,
|
20
20
|
:signature
|
21
21
|
|
22
|
-
def initialize(
|
23
|
-
super
|
22
|
+
def initialize(signature:, namespaces: [], **args)
|
23
|
+
super(**args)
|
24
24
|
|
25
25
|
self.basepath = Chamber.configuration.basepath
|
26
|
-
self.namespaces =
|
27
|
-
self.signature =
|
26
|
+
self.namespaces = namespaces
|
27
|
+
self.signature = signature
|
28
28
|
end
|
29
29
|
|
30
|
-
# rubocop:disable
|
30
|
+
# rubocop:disable Layout/LineLength, Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
|
31
31
|
def call
|
32
32
|
key_pairs = namespaces.map do |namespace|
|
33
33
|
Chamber::KeyPair.new(namespace: namespace,
|
@@ -78,21 +78,25 @@ class Initialize < Chamber::Commands::Base
|
|
78
78
|
shell.say 'Your signature keys, which will be used for verification, are located at:'
|
79
79
|
shell.say ''
|
80
80
|
shell.say ' * Public Key: '
|
81
|
-
shell.say signature_key_pair
|
82
|
-
public_key_filepath
|
83
|
-
relative_path_from(Pathname.pwd),
|
81
|
+
shell.say signature_key_pair
|
82
|
+
.public_key_filepath
|
83
|
+
.relative_path_from(Pathname.pwd),
|
84
|
+
:yellow
|
84
85
|
shell.say ' * Private Key: '
|
85
|
-
shell.say signature_key_pair
|
86
|
-
unencrypted_private_key_filepath
|
87
|
-
relative_path_from(Pathname.pwd),
|
86
|
+
shell.say signature_key_pair
|
87
|
+
.unencrypted_private_key_filepath
|
88
|
+
.relative_path_from(Pathname.pwd),
|
89
|
+
:yellow
|
88
90
|
shell.say ' * Encrypted Private Key: '
|
89
|
-
shell.say signature_key_pair
|
90
|
-
encrypted_private_key_filepath
|
91
|
-
relative_path_from(Pathname.pwd),
|
91
|
+
shell.say signature_key_pair
|
92
|
+
.encrypted_private_key_filepath
|
93
|
+
.relative_path_from(Pathname.pwd),
|
94
|
+
:yellow
|
92
95
|
shell.say ' * Encrypted Passphrase: '
|
93
|
-
shell.say signature_key_pair
|
94
|
-
encrypted_private_key_passphrase_filepath
|
95
|
-
relative_path_from(Pathname.pwd),
|
96
|
+
shell.say signature_key_pair
|
97
|
+
.encrypted_private_key_passphrase_filepath
|
98
|
+
.relative_path_from(Pathname.pwd),
|
99
|
+
:yellow
|
96
100
|
|
97
101
|
shell.say ''
|
98
102
|
shell.say 'The signature private keys should be thought of separately from the other'
|
@@ -153,7 +157,7 @@ class Initialize < Chamber::Commands::Base
|
|
153
157
|
shell.say '--------------------------------------------------------------------------------'
|
154
158
|
shell.say ''
|
155
159
|
end
|
156
|
-
# rubocop:enable
|
160
|
+
# rubocop:enable Layout/LineLength, Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
|
157
161
|
|
158
162
|
protected
|
159
163
|
|
@@ -178,7 +182,7 @@ class Initialize < Chamber::Commands::Base
|
|
178
182
|
end
|
179
183
|
|
180
184
|
def append_to_gitignore
|
181
|
-
::FileUtils.touch
|
185
|
+
::FileUtils.touch(gitignore_filepath)
|
182
186
|
|
183
187
|
gitignore_contents = ::File.read(gitignore_filepath)
|
184
188
|
|
@@ -206,7 +210,7 @@ class Initialize < Chamber::Commands::Base
|
|
206
210
|
|
207
211
|
def gem_path
|
208
212
|
@gem_path ||= Pathname.new(
|
209
|
-
::File.expand_path('
|
213
|
+
::File.expand_path('../../..', __dir__),
|
210
214
|
)
|
211
215
|
end
|
212
216
|
|