devdnsd 3.0.7 → 3.0.8

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.
data/doc/_index.html CHANGED
@@ -237,7 +237,7 @@
237
237
  </div>
238
238
 
239
239
  <div id="footer">
240
- Generated on Sat Mar 8 11:03:19 2014 by
240
+ Generated on Sat Mar 8 15:25:56 2014 by
241
241
  <a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
242
242
  0.8.7.3 (ruby-2.1.0).
243
243
  </div>
data/doc/file.README.html CHANGED
@@ -105,7 +105,7 @@
105
105
 
106
106
  <h2 id="configuration">Configuration</h2>
107
107
 
108
- <p>By defaults, DevDNSd uses a configuration file in <code>~/.devdnsd_config</code>, but you can change the path using the <code>--config</code> switch.</p>
108
+ <p>By defaults, DevDNSd uses a configuration file in <code>~/.devdnsd/default.conf</code>, but you can change the path using the <code>--config</code> switch.</p>
109
109
 
110
110
  <p>The file is a plain Ruby file with a single <code>config</code> object that supports the following directives.</p>
111
111
 
@@ -166,7 +166,7 @@ This argument is ignored if you pass the block, as it assumes that the second ar
166
166
  </div></div>
167
167
 
168
168
  <div id="footer">
169
- Generated on Sat Mar 8 11:03:19 2014 by
169
+ Generated on Sat Mar 8 15:25:56 2014 by
170
170
  <a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
171
171
  0.8.7.3 (ruby-2.1.0).
172
172
  </div>
data/doc/index.html CHANGED
@@ -105,7 +105,7 @@
105
105
 
106
106
  <h2 id="configuration">Configuration</h2>
107
107
 
108
- <p>By defaults, DevDNSd uses a configuration file in <code>~/.devdnsd_config</code>, but you can change the path using the <code>--config</code> switch.</p>
108
+ <p>By defaults, DevDNSd uses a configuration file in <code>~/.devdnsd/default.conf</code>, but you can change the path using the <code>--config</code> switch.</p>
109
109
 
110
110
  <p>The file is a plain Ruby file with a single <code>config</code> object that supports the following directives.</p>
111
111
 
@@ -166,7 +166,7 @@ This argument is ignored if you pass the block, as it assumes that the second ar
166
166
  </div></div>
167
167
 
168
168
  <div id="footer">
169
- Generated on Sat Mar 8 11:03:19 2014 by
169
+ Generated on Sat Mar 8 15:25:56 2014 by
170
170
  <a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
171
171
  0.8.7.3 (ruby-2.1.0).
172
172
  </div>
@@ -103,7 +103,7 @@
103
103
  </div>
104
104
 
105
105
  <div id="footer">
106
- Generated on Sat Mar 8 11:03:19 2014 by
106
+ Generated on Sat Mar 8 15:25:56 2014 by
107
107
  <a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
108
108
  0.8.7.3 (ruby-2.1.0).
109
109
  </div>
@@ -73,7 +73,7 @@ module DevDNSd
73
73
  # @param command [String] The command to execute.
74
74
  # @return [Boolean] `true` if command succeeded, `false` otherwise.
75
75
  def execute_command(command)
76
- system(command)
76
+ system("#{command} 2&>1 > /dev/null")
77
77
  end
78
78
 
79
79
  # Updates DNS cache.
@@ -81,7 +81,15 @@ module DevDNSd
81
81
  # @return [Boolean] `true` if command succeeded, `false` otherwise.
82
82
  def dns_update
83
83
  @logger.info(i18n.dns_update)
84
- execute_command("dscacheutil -flushcache")
84
+
85
+ script = Tempfile.new("devdnsd-dns-cache-script")
86
+ script.write("dscacheutil -flushcache 2&>1 > /dev/null\n")
87
+ script.write("killall -9 mDNSResponder 2&>1 > /dev/null\n")
88
+ script.write("killall -9 mDNSResponderHelper 2&>1 > /dev/null\n")
89
+ script.close
90
+
91
+ Kernel.system("/usr/bin/osascript -e 'do shell script \"sh #{script.path}\" with administrator privileges' 2&>1 > /dev/null")
92
+ script.unlink
85
93
  end
86
94
 
87
95
  # Checks if we are running on MacOS X.
@@ -160,6 +168,8 @@ module DevDNSd
160
168
  # @return [Boolean] `true` if operation succeeded, `false` otherwise.
161
169
  def manage_installation(launch_agent, resolver_path, first_operation, second_operation, third_operation)
162
170
  rv = check_agent_available
171
+
172
+ logger.warn(replace_markers(i18n.admin_privileges_warning))
163
173
  rv = send(first_operation, launch_agent, resolver_path) if rv
164
174
  rv = send(second_operation, launch_agent, resolver_path) if rv
165
175
  rv = send(third_operation, launch_agent, resolver_path) if rv
@@ -203,33 +213,36 @@ module DevDNSd
203
213
  # @return [Boolean] `true` if operation succeeded, `false` otherwise.
204
214
  def create_resolver(_, resolver_path)
205
215
  begin
206
- logger.info(i18n.resolver_creating(resolver_path))
216
+ logger.info(replace_markers(i18n.resolver_creating(resolver_path)))
207
217
 
208
- ::File.open(resolver_path, "w") {|f|
209
- f.write("nameserver 127.0.0.1\n")
210
- f.write("port #{@config.port}")
211
- f.flush
212
- }
218
+ script = Tempfile.new("devdnsd-install-script")
219
+ script.write("mkdir -p '#{File.dirname(resolver_path)}'\n")
220
+ script.write("rm -rf '#{resolver_path}'\n")
221
+ script.write("echo 'nameserver 127.0.0.1\\nport #{@config.port}' >> '#{resolver_path}'")
222
+ script.close
213
223
 
224
+ Kernel.system("/usr/bin/osascript -e 'do shell script \"sh #{script.path}\" with administrator privileges' 2&>1 > /dev/null")
225
+ script.unlink
214
226
  true
215
- rescue
227
+ rescue Exception
216
228
  logger.error(i18n.resolver_creating_error)
217
229
  false
218
230
  end
219
231
  end
220
232
 
221
- # Writes a OSX resolver.
222
- #
223
- # @param resolver_path [String] The resolver path.
224
- def write_resolver(resolver_path)
225
- end
226
-
227
233
  # Deletes a OSX resolver.
228
234
  #
229
235
  # @param resolver_path [String] The resolver path.
230
236
  # @return [Boolean] `true` if operation succeeded, `false` otherwise.
231
237
  def delete_resolver(_, resolver_path)
232
- delete_file(resolver_path, :resolver_deleting, :resolver_deleting_error)
238
+ begin
239
+ logger.info(i18n.resolver_deleting(resolver_path))
240
+ Kernel.system("/usr/bin/osascript -e 'do shell script \"rm #{resolver_path}\" with administrator privileges' 2&>1 > /dev/null")
241
+ true
242
+ rescue Exception
243
+ logger.warn(i18n.resolver_deleting_error)
244
+ false
245
+ end
233
246
  end
234
247
 
235
248
  # Creates a OSX system agent.
@@ -238,7 +251,7 @@ module DevDNSd
238
251
  # @return [Boolean] `true` if operation succeeded, `false` otherwise.
239
252
  def create_agent(launch_agent, _)
240
253
  begin
241
- logger.info(i18n.agent_creating(launch_agent))
254
+ logger.info(replace_markers(i18n.agent_creating(launch_agent)))
242
255
  program, args = prepare_agent
243
256
 
244
257
  ::File.open(launch_agent, "w") {|f|
@@ -305,6 +318,14 @@ module DevDNSd
305
318
  false
306
319
  end
307
320
  end
321
+
322
+ # Replaces markers in a log message.
323
+ #
324
+ # @param message [String] The message to process.
325
+ # @return [String] The processed message.
326
+ def replace_markers(message)
327
+ @command.application.console.replace_markers(message)
328
+ end
308
329
  end
309
330
 
310
331
  # Methods to handle interfaces aliases.
@@ -464,7 +485,7 @@ module DevDNSd
464
485
  log_management(:run, prefix, type, locale.removing, locale.adding, address, config)
465
486
  rv = execute_command(command)
466
487
  labels = (type == :remove ? [locale.remove, locale.from] : [locale.add, locale.to])
467
- @logger.error(@command.application.console.replace_markers(locale.general_error(labels[0], address, labels[1], config.interface))) if !rv
488
+ @logger.error(replace_markers(locale.general_error(labels[0], address, labels[1], config.interface))) if !rv
468
489
  rv
469
490
  end
470
491
 
@@ -480,7 +501,7 @@ module DevDNSd
480
501
  def log_management(message, prefix, type, remove_label, add_label, address, config)
481
502
  locale = i18n
482
503
  labels = (type == :remove ? [remove_label, locale.from] : [add_label, locale.to])
483
- @logger.info(@command.application.console.replace_markers(i18n.send(message, prefix, labels[0], address, labels[1], config.interface)))
504
+ @logger.info(replace_markers(i18n.send(message, prefix, labels[0], address, labels[1], config.interface)))
484
505
  end
485
506
  end
486
507
 
@@ -624,7 +645,7 @@ module DevDNSd
624
645
  options = @command.application.get_options.reject {|_, v| v.nil? }
625
646
 
626
647
  # Setup logger
627
- @logger = Bovem::Logger.create(Bovem::Logger.get_real_file(options["log_file"]) || Bovem::Logger.default_file, Logger::INFO)
648
+ create_logger(options)
628
649
 
629
650
  # Open configuration
630
651
  read_configuration(options)
@@ -687,18 +708,59 @@ module DevDNSd
687
708
  end
688
709
 
689
710
  private
711
+ # Creates a logger.
712
+ #
713
+ # @param options [Hash] The configuration to use.
714
+ def create_logger(options)
715
+ warn_failure = false
716
+ orig_file = file = Bovem::Logger.get_real_file(options["log_file"]) || Bovem::Logger.default_file
717
+
718
+ if file.is_a?(String) then
719
+ file = File.absolute_path(File.expand_path(file))
720
+
721
+ begin
722
+ FileUtils.mkdir_p(File.dirname(file))
723
+ @logger = Bovem::Logger.create(file, Logger::INFO)
724
+ rescue
725
+ file = $stdout
726
+ warn_failure = true
727
+ end
728
+ end
729
+
730
+ @logger = Bovem::Logger.create(file, Logger::INFO)
731
+ @logger.warn(replace_markers(i18n.logging_failed(orig_file))) if @logger && warn_failure
732
+ @logger
733
+ end
734
+
690
735
  # Reads configuration.
691
736
  #
692
737
  # @param options [Hash] The configuration to read.
693
738
  def read_configuration(options)
739
+ path = ::File.absolute_path(File.expand_path(options["configuration"]))
740
+
694
741
  begin
695
- @config = DevDNSd::Configuration.new(options["configuration"], options, @logger)
742
+ @config = DevDNSd::Configuration.new(path, options, @logger)
743
+ ensure_directory_for(@config.log_file) if @config.log_file.is_a?(String)
744
+ ensure_directory_for(@config.pid_file)
745
+
696
746
  @logger = nil
697
747
  @logger = get_logger
698
748
  rescue Bovem::Errors::InvalidConfiguration => e
699
- logger = Bovem::Logger.create("STDERR")
749
+ logger = Bovem::Logger.create($stderr)
700
750
  logger.fatal(e.message)
701
- logger.warn(@command.application.console.replace_markers(i18n.application_create_config(options["configuration"])))
751
+ logger.warn(replace_markers(i18n.application_create_config(path)))
752
+ raise ::SystemExit
753
+ end
754
+ end
755
+
756
+ # Creates a folder for a file.
757
+ #
758
+ # @param [String] The path of the file.
759
+ def ensure_directory_for(path)
760
+ begin
761
+ FileUtils.mkdir_p(File.dirname(path))
762
+ rescue
763
+ @logger.warn(replace_markers(i18n.invalid_directory(File.dirname(path))))
702
764
  raise ::SystemExit
703
765
  end
704
766
  end
@@ -19,11 +19,11 @@ module DevDNSd
19
19
  # The TLD to manage. Default: `dev`.
20
20
  property :tld, default: "dev"
21
21
 
22
- # The PID file to use. Default: `/var/run/devdnsd.pid`.
23
- property :pid_file, default: "/var/log/devdnsd.pid"
22
+ # The PID file to use. Default: `~/.devdnsd/daemon.pid`.
23
+ property :pid_file, default: "~/.devdnsd/daemon.pid"
24
24
 
25
- # The file to log to. Default: `/var/log/devdnsd.log`.
26
- property :log_file, default: "/var/log/devdnsd.log"
25
+ # The file to log to. Default: `/var/log/daemon.log`.
26
+ property :log_file, default: "~/.devdnsd/daemon.log"
27
27
 
28
28
  # The minimum severity to log. Default: `Logger::INFO`.
29
29
  property :log_level, default: Logger::INFO
@@ -65,8 +65,13 @@ module DevDNSd
65
65
  super(file, overrides, logger)
66
66
 
67
67
  # Make sure some arguments are of correct type
68
- self.log_file = $stdout if log_file == "STDOUT"
69
- self.log_file = $stderr if log_file == "STDERR"
68
+ self.log_file = case log_file
69
+ when "STDOUT" then $stdout
70
+ when "STDERR" then $stderr
71
+ else File.absolute_path(File.expand_path(log_file))
72
+ end
73
+
74
+ self.pid_file = File.absolute_path(File.expand_path(pid_file))
70
75
  self.port = port.to_integer
71
76
  self.log_level = log_level.to_integer
72
77
 
@@ -16,7 +16,7 @@ module DevDNSd
16
16
  MINOR = 0
17
17
 
18
18
  # The patch version.
19
- PATCH = 7
19
+ PATCH = 8
20
20
 
21
21
  # The current version number of DevDNSd.
22
22
  STRING = [MAJOR, MINOR, PATCH].compact.join(".")
data/lib/devdnsd.rb CHANGED
@@ -12,6 +12,7 @@ require "mustache"
12
12
  require "ipaddr"
13
13
  require "fiber"
14
14
  require "plist"
15
+ require "tempfile"
15
16
 
16
17
  Lazier.load!(:object)
17
18
 
data/locales/en.yml CHANGED
@@ -17,6 +17,7 @@
17
17
  reply: "Reply is %1 with type %2."
18
18
  no_reply: "No reply found."
19
19
  no_agent: "Install DevDNSd as a local resolver is only available on MacOSX."
20
+ admin_privileges_warning: "Basing on your setup, the system might ask you up to twice to grant {mark=bright}osascript{/mark} application admin privileges."
20
21
  resolver_creating: "Creating the resolver in {mark=bright}%1{/mark} ..."
21
22
  resolver_creating_error: "Cannot create the resolver file."
22
23
  resolver_deleting: "Deleting the resolver %1 ..."
@@ -29,7 +30,8 @@
29
30
  agent_loading_error: "Cannot load the launch agent."
30
31
  agent_unloading: "Unloading the launch agent %1 ..."
31
32
  agent_unloading_error: "Cannot unload the launch agent."
32
- logging_failed: "Cannot log to {mark=bright}%1{/mark}. Exiting..."
33
+ logging_failed: "Cannot log to {mark=bright}%1{/mark}. Logging to terminal..."
34
+ invalid_directory: "Cannot write on directory {mark=bright}%1{/mark}. Exiting..."
33
35
  application_description: "A small DNS server to enable local domain resolution."
34
36
  application_help_configuration: "The configuration file to use. Default is \"~/.devdnsd_config\"."
35
37
  application_help_tld: "The TLD to handle. Default is \"dev\"."
data/locales/it.yml CHANGED
@@ -17,6 +17,7 @@
17
17
  reply: "La risposta è %1 con tipo %2."
18
18
  no_reply: "Nessuna risposta trovata."
19
19
  no_agent: "Installare DevDNSd come resolver locale è disponibile solo su MacOSX."
20
+ admin_privileges_warning: "In base alla tua configurazione, il sistema potrebbe chiederti fino a due volte di garantire all'applicazione {mark=bright}osascript{/mark} i privilegi di amministratore."
20
21
  resolver_creating: "Creo il resolver in {mark=bright}%1{/mark} ..."
21
22
  resolver_creating_error: "Impossible creare il file del resolver."
22
23
  resolver_deleting: "Cancello resolver %1 ..."
@@ -29,7 +30,8 @@
29
30
  agent_loading_error: "Impossibile avviare il launch agent."
30
31
  agent_unloading: "Fermo il launch agent %1 ..."
31
32
  agent_unloading_error: "Impossible fermare il launch agent."
32
- logging_failed: "Impossibile eseguire il logging in {mark=bright}%1{/mark}. Esco..."
33
+ logging_failed: "Impossibile eseguire il logging in {mark=bright}%1{/mark}. Eseguo il logging nel terminale..."
34
+ invalid_directory: "Impossibile scrivere sulla directory {mark=bright}%1{/mark}. Esco..."
33
35
  application_description: "Un piccolo server DNS per abilitare la risoluzione di domini locali."
34
36
  application_help_configuration: "Il file di configurazione da usare. Il valore predefinito è \"~/.devdnsd_config\"."
35
37
  application_help_tld: "Il TLD da gestiere. Il valore predefinito è \"dev\"."
@@ -17,7 +17,7 @@ describe DevDNSd::Application do
17
17
  option :configuration, [], {default: overrides["configuration"] || "/dev/null"}
18
18
  option :tld, [], {default: overrides["tld"] || "dev"}
19
19
  option :port, [], {type: Integer, default: overrides["port"] || 7771}
20
- option :pid_file, [:P, "pid-file"], {type: String, default: "/var/run/devdnsd.pid"}
20
+ option :pid_file, [:P, "pid-file"], {type: String, default: overrides["pid_file"] || "/var/run/devdnsd.pid"}
21
21
  option :log_file, [:l, "log-file"], {default: overrides["log_file"] || "/dev/null"}
22
22
  option :log_level, [:L, "log-level"], {type: Integer, default: overrides["log_level"] || 1}
23
23
  }, :en)
@@ -35,6 +35,12 @@ describe DevDNSd::Application do
35
35
  expect(application.logger).not_to be_nil
36
36
  end
37
37
 
38
+ it "should fallback logger to STDOUT" do
39
+ allow_any_instance_of(DevDNSd::Application).to receive(:read_configuration)
40
+ expect(Bovem::Logger).to receive(:create).with($stdout, Logger::INFO)
41
+ create_application({"log_file" => "/invalid/logger"})
42
+ end
43
+
38
44
  it "should setup the configuration" do
39
45
  expect(application.config).not_to be_nil
40
46
  end
@@ -50,6 +56,12 @@ describe DevDNSd::Application do
50
56
  expect { create_application({"configuration" => file.path, "log_file" => log_file}) }.to raise_error(::SystemExit)
51
57
  ::File.unlink(path)
52
58
  end
59
+
60
+ it "should abort when the log file is invalid" do
61
+ allow_any_instance_of(Bovem::Logger).to receive(:fatal)
62
+ allow_any_instance_of(Bovem::Logger).to receive(:warn)
63
+ expect { create_application({"pid_file" => "/invalid/pid", "log_file" => log_file}) }.to raise_error(::SystemExit)
64
+ end
53
65
  end
54
66
 
55
67
  describe ".run" do
@@ -339,8 +351,8 @@ describe DevDNSd::Application do
339
351
 
340
352
  describe "#dns_update" do
341
353
  it "should update the DNS cache" do
342
- allow(application).to receive(:execute_command).and_return("EXECUTED")
343
- expect(application.dns_update).to eq("EXECUTED")
354
+ allow(Kernel).to receive(:system).and_return("EXECUTED")
355
+ application.dns_update
344
356
  end
345
357
  end
346
358
 
@@ -559,6 +571,7 @@ describe DevDNSd::Application do
559
571
  before(:each) do
560
572
  allow(application).to receive(:is_osx?).and_return(true)
561
573
  allow(application).to receive(:execute_command)
574
+ expect(Kernel).to receive(:system).at_least(1)
562
575
  end
563
576
 
564
577
  it "should create the resolver" do
@@ -567,6 +580,7 @@ describe DevDNSd::Application do
567
580
  ::File.unlink(application.resolver_path) if ::File.exists?(application.resolver_path)
568
581
  ::File.unlink(application.launch_agent_path) if ::File.exists?(application.launch_agent_path)
569
582
 
583
+ allow(application).to receive(:create_resolver) {|_, path| FileUtils.touch(path) }
570
584
  application.action_install
571
585
  expect(::File.exists?(resolver_path)).to be_true
572
586
 
@@ -607,6 +621,8 @@ describe DevDNSd::Application do
607
621
  ::File.unlink(application.resolver_path) if ::File.exists?(application.resolver_path)
608
622
  ::File.unlink(application.launch_agent_path) if ::File.exists?(application.launch_agent_path)
609
623
 
624
+ allow(application).to receive(:create_agent).and_return(true)
625
+ expect_any_instance_of(R18n::Translation).to receive(:resolver_creating).and_raise(ArgumentError)
610
626
  expect(application.logger).to receive(:error).with("Cannot create the resolver file.")
611
627
  application.action_install
612
628
 
@@ -655,6 +671,7 @@ describe DevDNSd::Application do
655
671
  before(:each) do
656
672
  allow(application).to receive(:is_osx?).and_return(true)
657
673
  allow(application).to receive(:execute_command)
674
+ expect(Kernel).to receive(:system).at_least(1)
658
675
  end
659
676
 
660
677
  it "should remove the resolver" do
@@ -671,6 +688,23 @@ describe DevDNSd::Application do
671
688
  ::File.unlink(application.launch_agent_path) if ::File.exists?(application.launch_agent_path)
672
689
  end
673
690
 
691
+ it "should not remove an invalid resolver" do
692
+ allow(application).to receive(:resolver_path).and_return("/invalid/resolver")
693
+ allow(application).to receive(:launch_agent_path).and_return("/invalid/agent")
694
+ ::File.unlink(application.resolver_path) if ::File.exists?(application.resolver_path)
695
+ ::File.unlink(application.launch_agent_path) if ::File.exists?(application.launch_agent_path)
696
+
697
+ allow(application).to receive(:unload_agent).and_return(true)
698
+ expect_any_instance_of(R18n::Translation).to receive(:resolver_deleting).and_raise(ArgumentError)
699
+ expect(application.logger).to receive(:warn)
700
+ expect(application.logger).to receive(:warn).with("Cannot delete the resolver file.")
701
+
702
+ application.action_uninstall
703
+
704
+ ::File.unlink(application.resolver_path) if ::File.exists?(application.resolver_path)
705
+ ::File.unlink(application.launch_agent_path) if ::File.exists?(application.launch_agent_path)
706
+ end
707
+
674
708
  it "should remove the agent" do
675
709
  allow(application).to receive(:resolver_path).and_return(resolver_path)
676
710
  allow(application).to receive(:launch_agent_path).and_return(launch_agent_path)
@@ -14,11 +14,16 @@ describe DevDNSd::Configuration do
14
14
  expect(config.address).to eq("0.0.0.0")
15
15
  expect(config.port).to eq(7771)
16
16
  expect(config.tld).to eq("dev")
17
- expect(config.log_file).to eq("/var/log/devdnsd.log")
17
+ expect(config.log_file).to eq(File.absolute_path(File.expand_path("~/.devdnsd/daemon.log")))
18
18
  expect(config.log_level).to eq(::Logger::INFO)
19
19
  expect(config.rules.count).to eq(1)
20
20
  expect(config.foreground).to eq(false)
21
21
  end
22
+
23
+ it "should log to standard output or standard error" do
24
+ expect(DevDNSd::Configuration.new(nil, log_file: "STDOUT").log_file).to eq($stdout)
25
+ expect(DevDNSd::Configuration.new(nil, log_file: "STDERR").log_file).to eq($stderr)
26
+ end
22
27
  end
23
28
 
24
29
  describe "#add_rule" do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devdnsd
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.7
4
+ version: 3.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shogun
@@ -82,6 +82,7 @@ files:
82
82
  - ".travis-gemfile"
83
83
  - ".travis.yml"
84
84
  - ".yardopts"
85
+ - '1'
85
86
  - CHANGELOG.md
86
87
  - Gemfile
87
88
  - README.md