bundle_update_interactive 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 58fa0e12f3322d018b1fcaa788dd3b8a2c3354042fe1e6ab21313d0bc0d7518b
4
- data.tar.gz: a43866c8bad9bc0256b369cbf7e29db56c2b06293f2ae3a4a3e560ff75506e16
3
+ metadata.gz: 0cbe5e5a4fef836edfd7c1bc87939066c2f7460f4f1b883510ea065840747a54
4
+ data.tar.gz: 26bc0b61f7d3d2830c3fd687bd8e723486446cdbe6acf91455e6d02dfbb93df6
5
5
  SHA512:
6
- metadata.gz: 5325810c277c4a3f58d3308b9899c9097a2c723996e79030ccda96f79408c83734b2bcf9ae5920a1ee74f32837ebae91f4e946aa9ff08225db73bf6d06d69e91
7
- data.tar.gz: d241d58cb408a0b6294a8b56e010b8ffa841bdbb5ccf28c794fb2db4ebcc562f7ec486742ec36720ed9522c9cf2ce1d27d8a68619fefbee59d28810b3044e7fb
6
+ metadata.gz: d351f14505560566df5b09fd27ca1a537bca621208bb1859bb6ba1a430333a5889a2d8f1dfd6ce5b72ede3cecb3429c148a1e48a3a795c3290effe0e37c207d0
7
+ data.tar.gz: c649e223608a24c65715f7c430abc64841b41a8d765612a57c64cc5c7c56f4cc1327389916521c5f19af0742e177b4f3e5d31267aacbda3048efa5b9b9b0cc78
data/README.md CHANGED
@@ -12,6 +12,7 @@
12
12
  ---
13
13
 
14
14
  - [Quick start](#quick-start)
15
+ - [Options](#options)
15
16
  - [Features](#features)
16
17
  - [Prior art](#prior-art)
17
18
  - [Support](#support)
@@ -39,6 +40,11 @@ Or the shorthand:
39
40
  bundle ui
40
41
  ```
41
42
 
43
+ ## Options
44
+
45
+ - `--latest` [modifies the Gemfile if necessary to allow the latest gem versions](#allow-latest-versions)
46
+ - `-D` / `--exclusively=GROUP` [limits updatable gems by Gemfile groups](#limit-impact-by-gemfile-groups)
47
+
42
48
  ## Features
43
49
 
44
50
  ### Semver highlighting
@@ -69,6 +75,30 @@ When a newer version of a gem is available, but updating is not allowed due to a
69
75
 
70
76
  <img src="images/held-back.png" alt="Screenshot of rails and selenium-webdriver gems held back due to Gemfile requirements" width="717" />
71
77
 
78
+ To allow updates for gems that would normally be held back, use the `--latest` option (explained in the next section).
79
+
80
+ ### Allow latest versions
81
+
82
+ Normally `update-interactive` only makes changes to your Gemfile.lock. It honors the version restrictions ("pins") in your Gemfile and will not update your Gemfile.lock to have versions that are not allowed. However with the `--latest` flag, update-interactive can update the version pins in your Gemfile as well. Consider the following Gemfile:
83
+
84
+ ```ruby
85
+ gem "rails", "~> 7.1.0"
86
+ ```
87
+
88
+ Normally running `bundle update-interactive` will report that Rails is held back and therefore cannot be updated to the latest version. However, if you pass the `--latest` option like this:
89
+
90
+ ```
91
+ bundle update-interactive --latest
92
+ ```
93
+
94
+ Now Rails will be allowed to update. If you select to update Rails to the latest version (e.g. 7.2.0), `update-interactive` will modify the version requirement in your Gemfile to look like this:
95
+
96
+ ```ruby
97
+ gem "rails", "~> 7.2.0"
98
+ ```
99
+
100
+ In other words, it works similarly to `yarn upgrade-interactive --latest`.
101
+
72
102
  ### Changelogs
73
103
 
74
104
  `bundle update-interactive` will do its best to find an appropriate changelog for each gem.
@@ -10,6 +10,13 @@ module BundleUpdateInteractive
10
10
  system "#{bundle_bin.shellescape} update --conservative #{gems.flatten.map(&:shellescape).join(' ')}"
11
11
  end
12
12
 
13
+ def lock
14
+ success = system "#{bundle_bin.shellescape} lock"
15
+ raise "bundle lock command failed" unless success
16
+
17
+ true
18
+ end
19
+
13
20
  def read_updated_lockfile(*gems)
14
21
  command = ["#{bundle_bin.shellescape} lock --print"]
15
22
  command << "--conservative" if gems.any?
@@ -7,6 +7,8 @@ require "tty/screen"
7
7
 
8
8
  class BundleUpdateInteractive::CLI
9
9
  class MultiSelect
10
+ extend BundleUpdateInteractive::StringHelper
11
+
10
12
  class List < TTY::Prompt::MultiList
11
13
  def initialize(prompt, **options)
12
14
  @opener = options.delete(:opener)
@@ -47,7 +49,7 @@ class BundleUpdateInteractive::CLI
47
49
 
48
50
  def self.prompt_for_gems_to_update(outdated_gems, prompt: nil)
49
51
  table = Table.updatable(outdated_gems)
50
- title = "#{outdated_gems.length} gems can be updated."
52
+ title = "#{pluralize(outdated_gems.length, 'gem')} can be updated."
51
53
  opener = lambda do |gem|
52
54
  url = outdated_gems[gem].changelog_uri
53
55
  Launchy.open(url) unless url.nil?
@@ -48,6 +48,9 @@ module BundleUpdateInteractive
48
48
  Show updates for development and test gems only, leaving production gems untouched.
49
49
  #{pastel.green('bundle update-interactive')} #{pastel.yellow('-D')}
50
50
 
51
+ Allow the latest gem versions, ignoring Gemfile pins. May modify the Gemfile.
52
+ #{pastel.green('bundle update-interactive')} #{pastel.yellow('--latest')}
53
+
51
54
  HELP
52
55
  end
53
56
 
@@ -61,6 +64,9 @@ module BundleUpdateInteractive
61
64
  OptionParser.new do |parser|
62
65
  parser.summary_indent = " "
63
66
  parser.summary_width = 24
67
+ parser.on("--latest", "Modify the Gemfile to allow the latest gem versions") do
68
+ options.latest = true
69
+ end
64
70
  parser.on(
65
71
  "--exclusively=GROUP",
66
72
  "Update gems exclusively belonging to the specified Gemfile GROUP(s)"
@@ -84,9 +90,15 @@ module BundleUpdateInteractive
84
90
  end
85
91
 
86
92
  attr_accessor :exclusively
93
+ attr_writer :latest
87
94
 
88
95
  def initialize
89
96
  @exclusively = []
97
+ @latest = false
98
+ end
99
+
100
+ def latest?
101
+ @latest
90
102
  end
91
103
  end
92
104
  end
@@ -4,14 +4,9 @@ require "bundler"
4
4
 
5
5
  module BundleUpdateInteractive
6
6
  class CLI
7
- autoload :MultiSelect, "bundle_update_interactive/cli/multi_select"
8
- autoload :Options, "bundle_update_interactive/cli/options"
9
- autoload :Row, "bundle_update_interactive/cli/row"
10
- autoload :Table, "bundle_update_interactive/cli/table"
11
-
12
7
  def run(argv: ARGV) # rubocop:disable Metrics/AbcSize
13
8
  options = Options.parse(argv)
14
- report = generate_report(options)
9
+ report, updater = generate_report(options)
15
10
 
16
11
  puts_legend_and_withheld_gems(report) unless report.empty?
17
12
  puts("No gems to update.").then { return } if report.updatable_gems.empty?
@@ -22,13 +17,18 @@ module BundleUpdateInteractive
22
17
  puts "Updating the following gems."
23
18
  puts Table.updatable(selected_gems).render
24
19
  puts
25
- report.bundle_update!(*selected_gems.keys)
20
+ updater.apply_updates(*selected_gems.keys)
21
+ puts_gemfile_modified_notice if updater.modified_gemfile?
26
22
  rescue Exception => e # rubocop:disable Lint/RescueException
27
23
  handle_exception(e)
28
24
  end
29
25
 
30
26
  private
31
27
 
28
+ def puts_gemfile_modified_notice
29
+ puts BundleUpdateInteractive.pastel.yellow("Your Gemfile was changed to accommodate the latest gem versions.")
30
+ end
31
+
32
32
  def puts_legend_and_withheld_gems(report)
33
33
  puts
34
34
  puts legend
@@ -54,14 +54,17 @@ module BundleUpdateInteractive
54
54
 
55
55
  def generate_report(options)
56
56
  whisper "Resolving latest gem versions..."
57
- report = Reporter.new(groups: options.exclusively).generate_report
58
- return report if report.empty?
59
-
60
- whisper "Checking for security vulnerabilities..."
61
- report.scan_for_vulnerabilities!
57
+ updater_class = options.latest? ? Latest::Updater : Updater
58
+ updater = updater_class.new(groups: options.exclusively)
59
+
60
+ report = updater.generate_report
61
+ unless report.empty?
62
+ whisper "Checking for security vulnerabilities..."
63
+ report.scan_for_vulnerabilities!
64
+ progress "Finding changelogs", report.all_gems.values, &:changelog_uri
65
+ end
62
66
 
63
- progress "Finding changelogs", report.all_gems.values, &:changelog_uri
64
- report
67
+ [report, updater]
65
68
  end
66
69
 
67
70
  def whisper(message)
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BundleUpdateInteractive
4
+ module Latest
5
+ class GemRequirement
6
+ def self.parse(version)
7
+ return version if version.is_a?(GemRequirement)
8
+
9
+ _, operator, number = version.strip.match(/^([^\d\s]*)\s*(.+)/).to_a
10
+ operator = nil if operator.empty?
11
+
12
+ new(parts: number.split("."), operator: operator, parsed_from: version)
13
+ end
14
+
15
+ attr_reader :parts, :operator
16
+
17
+ def initialize(parts:, operator: nil, parsed_from: nil)
18
+ @parts = parts
19
+ @operator = operator
20
+ @parsed_from = parsed_from
21
+ end
22
+
23
+ def exact?
24
+ operator.nil?
25
+ end
26
+
27
+ def relax
28
+ return self if %w[!= > >=].include?(operator)
29
+ return self.class.parse(">= 0") if %w[< <=].include?(operator)
30
+
31
+ self.class.new(parts: parts, operator: ">=")
32
+ end
33
+
34
+ def shift(new_version) # rubocop:disable Metrics/AbcSize
35
+ return self.class.parse(new_version) if exact?
36
+ return self if Gem::Requirement.new(to_s).satisfied_by?(Gem::Version.new(new_version))
37
+ return self.class.new(parts: self.class.parse(new_version).parts, operator: "<=") if %w[< <=].include?(operator)
38
+
39
+ new_slice = self.class.parse(new_version).slice(parts.length)
40
+ self.class.new(parts: new_slice.parts, operator: "~>")
41
+ end
42
+
43
+ def slice(amount)
44
+ self.class.new(parts: parts[0, amount], operator: operator)
45
+ end
46
+
47
+ def to_s
48
+ parsed_from || [operator, parts.join(".")].compact.join(" ")
49
+ end
50
+
51
+ def ==(other)
52
+ return false unless other.is_a?(GemRequirement)
53
+
54
+ to_s == other.to_s
55
+ end
56
+
57
+ private
58
+
59
+ attr_reader :parsed_from
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BundleUpdateInteractive
4
+ module Latest
5
+ class GemfileEditor
6
+ def initialize(gemfile_path: "Gemfile", lockfile_path: "Gemfile.lock")
7
+ @gemfile_path = gemfile_path
8
+ @lockfile_path = lockfile_path
9
+ end
10
+
11
+ def with_relaxed_gemfile
12
+ original, modified = modify_gemfile { |_, requirement| requirement.relax }
13
+ yield
14
+ ensure
15
+ File.write(gemfile_path, original) if original && original != modified
16
+ end
17
+
18
+ def shift_gemfile
19
+ lockfile = Lockfile.parse(File.read(lockfile_path))
20
+ original, modified = modify_gemfile do |name, requirement|
21
+ lockfile_entry = lockfile[name]
22
+ requirement.shift(lockfile_entry.version.to_s) if lockfile_entry
23
+ end
24
+ original != modified
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :gemfile_path, :lockfile_path
30
+
31
+ def modify_gemfile(&block)
32
+ original_contents = File.read(gemfile_path)
33
+ new_contents = original_contents.dup
34
+
35
+ find_rewritable_gem_names(original_contents).each do |name|
36
+ rewrite_contents(name, new_contents, &block)
37
+ end
38
+
39
+ File.write(gemfile_path, new_contents) unless new_contents == original_contents
40
+ [original_contents, new_contents]
41
+ end
42
+
43
+ def find_rewritable_gem_names(contents)
44
+ Gemfile.parse(gemfile_path).dependencies.filter_map do |dep|
45
+ gem_name = dep.name
46
+ gem_name if gem_declaration_with_requirement_re(gem_name).match?(contents)
47
+ end
48
+ end
49
+
50
+ def rewrite_contents(gem_name, contents)
51
+ found = contents.sub!(gem_declaration_with_requirement_re(gem_name)) do |match|
52
+ version = Regexp.last_match[1]
53
+ match[Regexp.last_match.regexp, 1] = yield(gem_name, GemRequirement.parse(version)).to_s
54
+ match
55
+ end
56
+ raise "Can't rewrite version for #{gem_name}" unless found
57
+ end
58
+
59
+ def gem_declaration_re(gem_name)
60
+ /^\s*gem\s+["']#{Regexp.escape(gem_name)}["']/
61
+ end
62
+
63
+ def gem_declaration_with_requirement_re(gem_name)
64
+ /#{gem_declaration_re(gem_name)},\s*["']([^'"]+)["']/
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Extends the default Updater class to allow updating to the latest gem versions.
4
+ # Does this by using GemfileEditor to relax the Gemfile requirements before
5
+ # `find_updatable_gems` and `apply_updates` are called.
6
+ module BundleUpdateInteractive
7
+ module Latest
8
+ class Updater < BundleUpdateInteractive::Updater
9
+ def initialize(editor: GemfileEditor.new, **kwargs)
10
+ super(**kwargs)
11
+ @modified_gemfile = false
12
+ @editor = editor
13
+ end
14
+
15
+ def apply_updates(*, **)
16
+ result = editor.with_relaxed_gemfile { super }
17
+ @modified_gemfile = editor.shift_gemfile
18
+ BundlerCommands.lock
19
+ result
20
+ end
21
+
22
+ def modified_gemfile?
23
+ @modified_gemfile
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :editor
29
+
30
+ def find_updatable_gems
31
+ editor.with_relaxed_gemfile { super }
32
+ end
33
+
34
+ # Overrides the default Updater implementation.
35
+ # When updating the latest gems, by definition nothing is withheld, so we can skip this.
36
+ def find_withheld_gems(**)
37
+ {}
38
+ end
39
+ end
40
+ end
41
+ end
@@ -23,11 +23,6 @@ module BundleUpdateInteractive
23
23
  @all_gems ||= withheld_gems.merge(updatable_gems)
24
24
  end
25
25
 
26
- def expand_gems_with_exact_dependencies(*gem_names)
27
- gem_names.flatten!
28
- gem_names.flat_map { |name| [name, *current_lockfile[name].exact_dependencies] }.uniq
29
- end
30
-
31
26
  def scan_for_vulnerabilities!
32
27
  return false if all_gems.empty?
33
28
 
@@ -36,16 +31,12 @@ module BundleUpdateInteractive
36
31
  vulnerable_gem_names = Set.new(audit_report.vulnerable_gems.map(&:name))
37
32
 
38
33
  all_gems.each do |name, gem|
39
- gem.vulnerable = (vulnerable_gem_names & [name, *current_lockfile[name].exact_dependencies]).any?
34
+ exact_deps = current_lockfile && current_lockfile[name].exact_dependencies
35
+ gem.vulnerable = (vulnerable_gem_names & [name, *Array(exact_deps)]).any?
40
36
  end
41
37
  true
42
38
  end
43
39
 
44
- def bundle_update!(*gem_names)
45
- expanded_names = expand_gems_with_exact_dependencies(*gem_names)
46
- BundlerCommands.update_gems_conservatively(*expanded_names)
47
- end
48
-
49
40
  private
50
41
 
51
42
  attr_reader :current_lockfile
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/bundle_update_interactive/string_helper.rb
4
+ module BundleUpdateInteractive
5
+ module StringHelper
6
+ def pluralize(count, singular, plural=nil)
7
+ plural ||= "#{singular}s"
8
+ "#{count} #{count == 1 ? singular : plural}"
9
+ end
10
+ module_function :pluralize
11
+ end
12
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BundleUpdateInteractive
4
- class Reporter
4
+ class Updater
5
5
  def initialize(groups: [])
6
6
  @gemfile = Gemfile.parse
7
7
  @current_lockfile = Lockfile.parse
@@ -15,6 +15,16 @@ module BundleUpdateInteractive
15
15
  Report.new(current_lockfile: current_lockfile, updatable_gems: updatable_gems, withheld_gems: withheld_gems)
16
16
  end
17
17
 
18
+ def apply_updates(*gem_names)
19
+ expanded_names = expand_gems_with_exact_dependencies(*gem_names)
20
+ BundlerCommands.update_gems_conservatively(*expanded_names)
21
+ end
22
+
23
+ # Overridden by Latest::Updater subclass
24
+ def modified_gemfile?
25
+ false
26
+ end
27
+
18
28
  private
19
29
 
20
30
  attr_reader :gemfile, :current_lockfile, :candidate_gems
@@ -33,6 +43,20 @@ module BundleUpdateInteractive
33
43
  end
34
44
  end
35
45
 
46
+ def find_withheld_gems(exclude: [])
47
+ possibly_withheld = gemfile.dependencies.filter_map do |dep|
48
+ dep.name if dep.should_include? && !dep.requirement.none? # rubocop:disable Style/InverseMethods
49
+ end
50
+ possibly_withheld -= exclude
51
+ possibly_withheld &= candidate_gems unless candidate_gems.nil?
52
+
53
+ return {} if possibly_withheld.empty?
54
+
55
+ BundlerCommands.parse_outdated(*possibly_withheld).to_h do |name, newest|
56
+ [name, build_outdated_gem(name, newest, nil)]
57
+ end
58
+ end
59
+
36
60
  def build_outdated_gem(name, updated_version, updated_git_version)
37
61
  current_lockfile_entry = current_lockfile[name]
38
62
 
@@ -49,18 +73,9 @@ module BundleUpdateInteractive
49
73
  )
50
74
  end
51
75
 
52
- def find_withheld_gems(exclude: [])
53
- possibly_withheld = gemfile.dependencies.filter_map do |dep|
54
- dep.name if dep.should_include? && !dep.requirement.none? # rubocop:disable Style/InverseMethods
55
- end
56
- possibly_withheld -= exclude
57
- possibly_withheld &= candidate_gems unless candidate_gems.nil?
58
-
59
- return {} if possibly_withheld.empty?
60
-
61
- BundlerCommands.parse_outdated(*possibly_withheld).to_h do |name, newest|
62
- [name, build_outdated_gem(name, newest, nil)]
63
- end
76
+ def expand_gems_with_exact_dependencies(*gem_names)
77
+ gem_names.flatten!
78
+ gem_names.flat_map { |name| [name, *current_lockfile[name].exact_dependencies] }.uniq
64
79
  end
65
80
  end
66
81
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BundleUpdateInteractive
4
- VERSION = "0.7.0"
4
+ VERSION = "0.8.0"
5
5
  end
@@ -2,21 +2,13 @@
2
2
 
3
3
  require "pastel"
4
4
 
5
- module BundleUpdateInteractive
6
- autoload :BundlerCommands, "bundle_update_interactive/bundler_commands"
7
- autoload :ChangelogLocator, "bundle_update_interactive/changelog_locator"
8
- autoload :CLI, "bundle_update_interactive/cli"
9
- autoload :Error, "bundle_update_interactive/error"
10
- autoload :Gemfile, "bundle_update_interactive/gemfile"
11
- autoload :HTTP, "bundle_update_interactive/http"
12
- autoload :Lockfile, "bundle_update_interactive/lockfile"
13
- autoload :LockfileEntry, "bundle_update_interactive/lockfile_entry"
14
- autoload :OutdatedGem, "bundle_update_interactive/outdated_gem"
15
- autoload :Report, "bundle_update_interactive/report"
16
- autoload :Reporter, "bundle_update_interactive/reporter"
17
- autoload :SemverChange, "bundle_update_interactive/semver_change"
18
- autoload :VERSION, "bundle_update_interactive/version"
5
+ require "zeitwerk"
6
+ loader = Zeitwerk::Loader.for_gem
7
+ loader.inflector.inflect "cli" => "CLI"
8
+ loader.inflector.inflect "http" => "HTTP"
9
+ loader.setup
19
10
 
11
+ module BundleUpdateInteractive
20
12
  class << self
21
13
  attr_accessor :pastel
22
14
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bundle_update_interactive
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Brictson
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-08-14 00:00:00.000000000 Z
11
+ date: 2024-08-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: 0.8.2
97
+ - !ruby/object:Gem::Dependency
98
+ name: zeitwerk
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2.6'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.6'
97
111
  description:
98
112
  email:
99
113
  - opensource@mattbrictson.com
@@ -118,12 +132,16 @@ files:
118
132
  - lib/bundle_update_interactive/error.rb
119
133
  - lib/bundle_update_interactive/gemfile.rb
120
134
  - lib/bundle_update_interactive/http.rb
135
+ - lib/bundle_update_interactive/latest/gem_requirement.rb
136
+ - lib/bundle_update_interactive/latest/gemfile_editor.rb
137
+ - lib/bundle_update_interactive/latest/updater.rb
121
138
  - lib/bundle_update_interactive/lockfile.rb
122
139
  - lib/bundle_update_interactive/lockfile_entry.rb
123
140
  - lib/bundle_update_interactive/outdated_gem.rb
124
141
  - lib/bundle_update_interactive/report.rb
125
- - lib/bundle_update_interactive/reporter.rb
126
142
  - lib/bundle_update_interactive/semver_change.rb
143
+ - lib/bundle_update_interactive/string_helper.rb
144
+ - lib/bundle_update_interactive/updater.rb
127
145
  - lib/bundle_update_interactive/version.rb
128
146
  homepage: https://github.com/mattbrictson/bundle_update_interactive
129
147
  licenses:
@@ -149,7 +167,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
149
167
  - !ruby/object:Gem::Version
150
168
  version: '0'
151
169
  requirements: []
152
- rubygems_version: 3.5.16
170
+ rubygems_version: 3.5.11
153
171
  signing_key:
154
172
  specification_version: 4
155
173
  summary: Adds an update-interactive command to Bundler