create-ruby-gem 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8e3d2670ee80eb59448b75a05e221c84b1c07ffb190d09266ce43a1891797cf5
4
+ data.tar.gz: e2d3ecac59aa5046b697f003979e9c7b9a95fb2f434af157f703724111b42cf0
5
+ SHA512:
6
+ metadata.gz: 2c3c8820df470199ae8a162decdb72c96d8e9010bbce025270acc5c230ba260491855d50a68b4b35d1a6054363d7cf20d2223eb622312330e5b6a4aa52805f47
7
+ data.tar.gz: f2771d980f19ece12debdb81d7613889e1e59c715641782753e561c57cc6adba4a1bc13d6c48084095deba8f1f5a79c50bd4e0775ed0336138e654e4a5cdb654
data/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
7
+ and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
8
+
9
+ ## [Unreleased]
10
+
11
+ ## [0.1.0] - 2026-02-10
12
+
13
+ ### Added
14
+
15
+ - Interactive wizard for `bundle gem` with back/edit/cancel flow, command summary, and preset save prompt.
16
+ - Preset commands: `--list-presets`, `--show-preset`, `--delete-preset`, and `--preset` for non-interactive creation.
17
+ - Runtime diagnostics: `--doctor` and `--version`.
18
+ - Static Bundler compatibility matrix with explicit unsupported-version errors.
19
+ - Integration tests that execute real `bundle gem` runs in temporary directories.
20
+ - CI matrix testing Bundler versions across Ruby 3.2–4.0.
21
+ - YARD documentation for all source files.
22
+ - Production-ready README with compatibility matrix, CLI reference, and preset examples.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Leonid Svyatov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # create-ruby-gem [![Gem Version](https://img.shields.io/gem/v/create-ruby-gem)](https://rubygems.org/gems/create-ruby-gem) [![CI](https://github.com/svyatov/create-ruby-gem/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/svyatov/create-ruby-gem/actions?query=workflow%3ACI)
2
+
3
+ > Create Ruby gems with an interactive CLI wizard, which remembers your choices! No more `bundle gem` flags look-ups 🙌
4
+
5
+ ## Table of Contents
6
+
7
+ - [Supported Versions](#supported-versions)
8
+ - [Installation](#installation)
9
+ - [Quick Start](#quick-start)
10
+ - [Interactive Wizard](#interactive-wizard)
11
+ - [Preset System](#preset-system)
12
+ - [Compatibility Matrix](#compatibility-matrix)
13
+ - [Configuration](#configuration)
14
+ - [CLI Reference](#cli-reference)
15
+ - [Development](#development)
16
+ - [Contributing](#contributing)
17
+ - [Changelog](#changelog)
18
+ - [Versioning](#versioning)
19
+ - [License](#license)
20
+
21
+ ## Supported Versions
22
+
23
+ Ruby 3.2+ and Bundler 2.4+ are required.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ gem install create-ruby-gem
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ ```bash
34
+ create-ruby-gem my_awesome_gem
35
+ ```
36
+
37
+ The wizard detects your Ruby and Bundler versions, walks you through every supported option, shows a summary of the `bundle gem` command it will run, and lets you edit or confirm before executing.
38
+
39
+ ## Interactive Wizard
40
+
41
+ Running `create-ruby-gem <name>` starts the step-by-step wizard:
42
+
43
+ 1. **Version detection** — detects Ruby, RubyGems, and Bundler versions at runtime.
44
+ 2. **Option filtering** — shows only options your Bundler version supports (see [Compatibility Matrix](#compatibility-matrix)).
45
+ 3. **Smart defaults** — uses your last-used choices as defaults, falling back to Bundler's own settings.
46
+ 4. **Back-navigation** — press `Ctrl+B` to revisit the previous step.
47
+ 5. **Summary** — displays the exact `bundle gem` command with color-coded arguments.
48
+ 6. **Edit-again loop** — choose "edit again" to change options, or "create" to execute.
49
+ 7. **Preset save prompt** — after creation, optionally save your choices as a named preset.
50
+
51
+ Press `Ctrl+C` at any time to exit.
52
+
53
+ ## Preset System
54
+
55
+ Save, load, and manage option presets:
56
+
57
+ ```bash
58
+ # Save options as a preset during gem creation
59
+ create-ruby-gem my_gem --save-preset oss-defaults
60
+
61
+ # Create a gem using a saved preset (non-interactive)
62
+ create-ruby-gem my_gem --preset oss-defaults
63
+
64
+ # List all saved presets
65
+ create-ruby-gem --list-presets
66
+
67
+ # Show a preset's options
68
+ create-ruby-gem --show-preset oss-defaults
69
+
70
+ # Delete a preset
71
+ create-ruby-gem --delete-preset oss-defaults
72
+ ```
73
+
74
+ ## Compatibility Matrix
75
+
76
+ Options available depend on your Bundler version. The wizard automatically hides unsupported options.
77
+
78
+ | Option | Bundler 2.4–2.x | Bundler 3.x | Bundler 4.x |
79
+ |--------|:---:|:---:|:---:|
80
+ | `--exe` / `--no-exe` | ✓ | ✓ | ✓ |
81
+ | `--coc` / `--no-coc` | ✓ | ✓ | ✓ |
82
+ | `--changelog` / `--no-changelog` | — | ✓ | ✓ |
83
+ | `--ext` | c | c | c, go, rust |
84
+ | `--git` | ✓ | ✓ | ✓ |
85
+ | `--github-username` | ✓ | ✓ | ✓ |
86
+ | `--mit` / `--no-mit` | ✓ | ✓ | ✓ |
87
+ | `--test` | minitest, rspec, test-unit | minitest, rspec, test-unit | minitest, rspec, test-unit |
88
+ | `--ci` | circle, github, gitlab | circle, github, gitlab | circle, github, gitlab |
89
+ | `--linter` | — | rubocop, standard | rubocop, standard |
90
+ | `--edit` | ✓ | ✓ | ✓ |
91
+ | `--bundle` / `--no-bundle` | ✓ | ✓ | ✓ |
92
+
93
+ ## Configuration
94
+
95
+ Config is stored at `~/.config/create-ruby-gem/config.yml` (or `$XDG_CONFIG_HOME/create-ruby-gem/config.yml`).
96
+
97
+ The file contains:
98
+
99
+ - `version` — schema version (currently `1`)
100
+ - `last_used` — the options from your most recent gem creation
101
+ - `presets` — named option sets saved via `--save-preset` or the post-creation prompt
102
+
103
+ ## CLI Reference
104
+
105
+ | Flag | Description |
106
+ |------|-------------|
107
+ | `<name>` | Gem name (prompted interactively if omitted) |
108
+ | `--preset NAME` | Use a saved preset (non-interactive) |
109
+ | `--save-preset NAME` | Save the selected options as a preset |
110
+ | `--list-presets` | List all saved preset names |
111
+ | `--show-preset NAME` | Show a preset's options |
112
+ | `--delete-preset NAME` | Delete a saved preset |
113
+ | `--dry-run` | Print the `bundle gem` command without executing |
114
+ | `--bundler-version VERSION` | Override the detected Bundler version |
115
+ | `--doctor` | Print runtime versions and supported options |
116
+ | `--version` | Print the create-ruby-gem version |
117
+
118
+ ## Development
119
+
120
+ After checking out the repo, run `bin/setup` to install dependencies.
121
+ Then, run `bundle exec rake` to run the tests and linter.
122
+ You can also run `bin/console` for an interactive prompt.
123
+
124
+ ```bash
125
+ bundle exec rake # tests + rubocop (default task)
126
+ bundle exec rake spec # tests only
127
+ bundle exec rake yard # generate YARD docs into doc/
128
+ exe/create-ruby-gem # run the CLI locally
129
+ ```
130
+
131
+ ## Contributing
132
+
133
+ 1. Fork it
134
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
135
+ 3. Make your changes and run tests (`bundle exec rake`)
136
+ 4. Commit using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format (`git commit -m 'feat: add some feature'`)
137
+ 5. Push to the branch (`git push origin my-new-feature`)
138
+ 6. Create a new Pull Request
139
+
140
+ ## Changelog
141
+
142
+ See [CHANGELOG.md](CHANGELOG.md) for a detailed history of changes, following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format.
143
+
144
+ ## Versioning
145
+
146
+ This project follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html).
147
+
148
+ ## License
149
+
150
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require 'bundler/setup'
6
+ rescue LoadError
7
+ nil
8
+ end
9
+
10
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
11
+ require 'create_ruby_gem'
12
+
13
+ exit(CreateRubyGem::CLI.start(ARGV))
@@ -0,0 +1,360 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+
5
+ module CreateRubyGem
6
+ # Main entry point that orchestrates the entire create-ruby-gem flow.
7
+ #
8
+ # Parses CLI flags, detects runtime versions, runs the interactive wizard
9
+ # (or loads a preset), builds the +bundle gem+ command, and executes it.
10
+ # All collaborators are injected via constructor for testability.
11
+ #
12
+ # @example Run from the command line
13
+ # CreateRubyGem::CLI.start(ARGV)
14
+ class CLI
15
+ # Constructs and runs a CLI instance with the given arguments.
16
+ #
17
+ # @param argv [Array<String>] command-line arguments
18
+ # @param out [IO] standard output stream
19
+ # @param err [IO] standard error stream
20
+ # @param store [Config::Store] configuration persistence
21
+ # @param detector [Detection::Runtime] runtime version detector
22
+ # @param runner [Runner, nil] command runner (built from +out+ if nil)
23
+ # @param prompter [UI::Prompter, nil] user interaction handler
24
+ # @param palette [UI::Palette, nil] color palette for terminal output
25
+ # @return [Integer] exit code (0 on success, non-zero on failure)
26
+ def self.start(
27
+ argv = ARGV,
28
+ out: $stdout,
29
+ err: $stderr,
30
+ store: Config::Store.new,
31
+ detector: Detection::Runtime.new,
32
+ runner: nil,
33
+ prompter: nil,
34
+ palette: nil
35
+ )
36
+ instance = new(
37
+ argv: argv,
38
+ out: out,
39
+ err: err,
40
+ store: store,
41
+ detector: detector,
42
+ runner: runner || Runner.new(out: out),
43
+ prompter: prompter,
44
+ palette: palette
45
+ )
46
+ instance.run
47
+ end
48
+
49
+ # @param argv [Array<String>] command-line arguments
50
+ # @param out [IO] standard output stream
51
+ # @param err [IO] standard error stream
52
+ # @param store [Config::Store] configuration persistence
53
+ # @param detector [Detection::Runtime] runtime version detector
54
+ # @param runner [Runner] command runner
55
+ # @param prompter [UI::Prompter, nil] user interaction handler
56
+ # @param palette [UI::Palette, nil] color palette for terminal output
57
+ def initialize(argv:, out:, err:, store:, detector:, runner:, prompter:, palette:)
58
+ @argv = argv.dup
59
+ @out = out
60
+ @err = err
61
+ @store = store
62
+ @detector = detector
63
+ @runner = runner
64
+ @prompter = prompter
65
+ @palette = palette || UI::Palette.new
66
+ @options = {}
67
+ end
68
+
69
+ # Runs the CLI: parses flags, dispatches to the appropriate action,
70
+ # and returns an exit code.
71
+ #
72
+ # @return [Integer] exit code (0 success, 1 error, 130 interrupt)
73
+ def run
74
+ parse_options!
75
+
76
+ validate_top_level_flags!
77
+
78
+ return print_version if @options[:version]
79
+ return doctor if @options[:doctor]
80
+ return list_presets if @options[:list_presets]
81
+ return show_preset if @options[:show_preset]
82
+ return delete_preset if @options[:delete_preset]
83
+
84
+ runtime_versions = resolved_runtime_versions
85
+ compatibility_entry = Compatibility::Matrix.for(runtime_versions.bundler)
86
+ builder = CommandBuilder.new(compatibility_entry: compatibility_entry)
87
+ bundler_defaults = Detection::BundlerDefaults.new.detect
88
+ last_used = symbolize_keys(@store.last_used)
89
+
90
+ gem_name = resolve_gem_name!
91
+ selected_options =
92
+ if @options[:preset]
93
+ load_preset_options!
94
+ else
95
+ run_interactive_wizard!(
96
+ gem_name: gem_name,
97
+ builder: builder,
98
+ compatibility_entry: compatibility_entry,
99
+ last_used: last_used,
100
+ runtime_versions: runtime_versions,
101
+ bundler_defaults: bundler_defaults
102
+ )
103
+ end
104
+
105
+ Options::Validator.new(compatibility_entry).validate!(gem_name: gem_name, options: selected_options)
106
+ command = builder.build(gem_name: gem_name, options: selected_options)
107
+ @runner.run!(command, dry_run: @options[:dry_run])
108
+ @store.save_last_used(selected_options)
109
+ save_preset_if_requested(selected_options)
110
+ 0
111
+ rescue OptionParser::ParseError, Error => e
112
+ @err.puts(e.message)
113
+ 1
114
+ rescue Interrupt
115
+ @err.puts(::CLI::UI.fmt('{{green:See ya!}}'))
116
+ 130
117
+ end
118
+
119
+ private
120
+
121
+ attr_reader :palette
122
+
123
+ # @return [void]
124
+ def parse_options!
125
+ OptionParser.new do |parser|
126
+ parser.on('--preset NAME') { |value| @options[:preset] = value }
127
+ parser.on('--save-preset NAME') { |value| @options[:save_preset] = value }
128
+ parser.on('--list-presets') { @options[:list_presets] = true }
129
+ parser.on('--show-preset NAME') { |value| @options[:show_preset] = value }
130
+ parser.on('--delete-preset NAME') { |value| @options[:delete_preset] = value }
131
+ parser.on('--doctor') { @options[:doctor] = true }
132
+ parser.on('--version') { @options[:version] = true }
133
+ parser.on('--dry-run') { @options[:dry_run] = true }
134
+ parser.on('--bundler-version VERSION') { |value| @options[:bundler_version] = value }
135
+ end.parse!(@argv)
136
+ end
137
+
138
+ # @raise [ValidationError] if conflicting flags are combined
139
+ # @return [void]
140
+ def validate_top_level_flags!
141
+ conflicting_create_action = @options[:preset] || @options[:save_preset] || !@argv.empty?
142
+ query_action = @options[:list_presets] || @options[:show_preset]
143
+ if query_action && (@options[:delete_preset] || conflicting_create_action)
144
+ raise ValidationError, 'Preset query options cannot be combined with other actions'
145
+ end
146
+
147
+ if @options[:delete_preset] && conflicting_create_action
148
+ raise ValidationError, '--delete-preset cannot be combined with create actions'
149
+ end
150
+
151
+ if @options[:doctor] && (query_action || @options[:delete_preset] || conflicting_create_action)
152
+ raise ValidationError, '--doctor cannot be combined with other actions'
153
+ end
154
+
155
+ conflicting_action = @options[:doctor] || query_action
156
+ conflicting_action ||= @options[:delete_preset]
157
+ conflicting_action ||= conflicting_create_action
158
+ return unless @options[:version] && conflicting_action
159
+
160
+ raise ValidationError, '--version cannot be combined with other actions'
161
+ end
162
+
163
+ # @return [Integer] exit code
164
+ def print_version
165
+ @out.puts(VERSION)
166
+ 0
167
+ end
168
+
169
+ # Prints runtime version info and supported options.
170
+ #
171
+ # @return [Integer] exit code
172
+ def doctor
173
+ runtime_versions = resolved_runtime_versions
174
+ @out.puts("ruby: #{runtime_versions.ruby}")
175
+ @out.puts("rubygems: #{runtime_versions.rubygems}")
176
+ @out.puts("bundler: #{runtime_versions.bundler}")
177
+ entry = Compatibility::Matrix.for(runtime_versions.bundler)
178
+ options = Options::Catalog::ORDER.select { |key| entry.supports_option?(key) }
179
+ @out.puts("supported options: #{options.join(', ')}")
180
+ 0
181
+ rescue UnsupportedBundlerVersionError => e
182
+ @err.puts(e.message)
183
+ 1
184
+ end
185
+
186
+ # @return [Integer] exit code
187
+ def list_presets
188
+ @store.preset_names.each { |name| @out.puts(name) }
189
+ 0
190
+ end
191
+
192
+ # @return [Integer] exit code
193
+ # @raise [ValidationError] if the preset does not exist
194
+ def show_preset
195
+ preset = @store.preset(@options[:show_preset])
196
+ raise ValidationError, "Preset not found: #{@options[:show_preset]}" unless preset
197
+
198
+ @out.puts(@options[:show_preset])
199
+ preset.sort.each { |key, value| @out.puts(" #{key}: #{value.inspect}") }
200
+ 0
201
+ end
202
+
203
+ # @return [Integer] exit code
204
+ def delete_preset
205
+ @store.delete_preset(@options[:delete_preset])
206
+ 0
207
+ end
208
+
209
+ # @return [String] gem name from argv or interactive prompt
210
+ # @raise [ValidationError] if gem name is required but missing
211
+ def resolve_gem_name!
212
+ gem_name = @argv.shift
213
+ return gem_name if gem_name && !gem_name.empty?
214
+
215
+ raise ValidationError, 'Gem name is required when --preset is provided' if @options[:preset]
216
+
217
+ prompter.text('Gem name:', allow_empty: false)
218
+ end
219
+
220
+ # @return [Hash{Symbol => Object}] preset options
221
+ # @raise [ValidationError] if the preset does not exist
222
+ def load_preset_options!
223
+ preset = @store.preset(@options[:preset])
224
+ raise ValidationError, "Preset not found: #{@options[:preset]}" unless preset
225
+
226
+ symbolize_keys(preset)
227
+ end
228
+
229
+ # Runs the interactive wizard loop with summary + edit-again flow.
230
+ #
231
+ # @param gem_name [String]
232
+ # @param builder [CommandBuilder]
233
+ # @param compatibility_entry [Compatibility::Matrix::Entry]
234
+ # @param last_used [Hash{Symbol => Object}]
235
+ # @param runtime_versions [Detection::RuntimeInfo]
236
+ # @param bundler_defaults [Hash{Symbol => Object}]
237
+ # @return [Hash{Symbol => Object}] selected options
238
+ def run_interactive_wizard!(
239
+ gem_name:,
240
+ builder:,
241
+ compatibility_entry:,
242
+ last_used:,
243
+ runtime_versions:,
244
+ bundler_defaults:
245
+ )
246
+ defaults = last_used
247
+ prompter.frame('Controls') do
248
+ prompter.say("Press #{palette.color(:control_back, 'Ctrl+B')} to go back one step.")
249
+ prompter.say("Press #{palette.color(:control_exit, 'Ctrl+C')} to exit.")
250
+ end
251
+ loop do
252
+ selected_options = Wizard.new(
253
+ compatibility_entry: compatibility_entry,
254
+ defaults: defaults,
255
+ bundler_defaults: bundler_defaults,
256
+ prompter: prompter
257
+ ).run
258
+
259
+ command = builder.build(gem_name: gem_name, options: selected_options)
260
+ show_summary(
261
+ command: command,
262
+ runtime_versions: runtime_versions
263
+ )
264
+ action = prompter.choose('Next step', options: ['create', 'edit again'], default: 'create')
265
+ return selected_options if action == 'create'
266
+
267
+ defaults = selected_options
268
+ end
269
+ end
270
+
271
+ # @param command [Array<String>]
272
+ # @param runtime_versions [Detection::RuntimeInfo]
273
+ # @return [void]
274
+ def show_summary(command:, runtime_versions:)
275
+ command_name = command.first(2).join(' ')
276
+ gem_name = command[2].to_s
277
+ args = command[3..] || []
278
+
279
+ prompter.frame('create-ruby-gem summary') do
280
+ runtime_line = "#{palette.color(:summary_label, 'Runtime:')} "
281
+ runtime_line += format_runtime_versions(runtime_versions)
282
+ prompter.say(runtime_line)
283
+ command_line = "#{palette.color(:command_base, command_name)} "
284
+ command_line += palette.color(:command_gem, gem_name)
285
+ command_line += " #{format_args(args)}" unless args.empty?
286
+ prompter.say("#{palette.color(:summary_label, 'Command:')} #{command_line}")
287
+ end
288
+ end
289
+
290
+ # @param runtime_versions [Detection::RuntimeInfo]
291
+ # @return [String]
292
+ def format_runtime_versions(runtime_versions)
293
+ [
294
+ ['ruby', runtime_versions.ruby],
295
+ ['rubygems', runtime_versions.rubygems],
296
+ ['bundler', runtime_versions.bundler]
297
+ ].map do |name, version|
298
+ "#{palette.color(:runtime_name, name)} #{palette.color(:runtime_value, version.to_s)}"
299
+ end.join(', ')
300
+ end
301
+
302
+ # @param args [Array<String>]
303
+ # @return [String]
304
+ def format_args(args)
305
+ args.map { |argument| format_argument(argument) }.join(' ')
306
+ end
307
+
308
+ # @param argument [String]
309
+ # @return [String]
310
+ def format_argument(argument)
311
+ return palette.color(:arg_value, argument) unless argument.start_with?('--')
312
+ return palette.color(:arg_name, argument) unless argument.include?('=')
313
+
314
+ name, value = argument.split('=', 2)
315
+ "#{palette.color(:arg_name, name)}#{palette.color(:arg_eq, '=')}#{palette.color(:arg_value, value)}"
316
+ end
317
+
318
+ # @param selected_options [Hash{Symbol => Object}]
319
+ # @return [void]
320
+ def save_preset_if_requested(selected_options)
321
+ if @options[:save_preset]
322
+ @store.save_preset(@options[:save_preset], selected_options)
323
+ return
324
+ end
325
+
326
+ return if @options[:preset]
327
+ return unless prompter.confirm('Save these options as a preset?', default: false)
328
+
329
+ preset_name = prompter.text('Preset name:', allow_empty: false)
330
+ @store.save_preset(preset_name, selected_options)
331
+ end
332
+
333
+ # @param hash [Hash{String => Object}]
334
+ # @return [Hash{Symbol => Object}]
335
+ def symbolize_keys(hash)
336
+ hash.transform_keys(&:to_sym)
337
+ end
338
+
339
+ # @return [Detection::RuntimeInfo]
340
+ def resolved_runtime_versions
341
+ if @options[:bundler_version]
342
+ return Detection::RuntimeInfo.new(
343
+ ruby: Gem::Version.new(RUBY_VERSION),
344
+ rubygems: Gem::Version.new(Gem::VERSION),
345
+ bundler: Gem::Version.new(@options[:bundler_version])
346
+ )
347
+ end
348
+
349
+ @detector.detect!
350
+ end
351
+
352
+ # @return [UI::Prompter]
353
+ def prompter
354
+ @prompter ||= begin
355
+ UI::Prompter.setup!
356
+ UI::Prompter.new(out: @out)
357
+ end
358
+ end
359
+ end
360
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CreateRubyGem
4
+ # Converts a gem name and option hash into a +bundle gem+ command array.
5
+ #
6
+ # @example
7
+ # entry = Compatibility::Matrix.for('3.1.0')
8
+ # builder = CommandBuilder.new(compatibility_entry: entry)
9
+ # builder.build(gem_name: 'my_gem', options: { exe: true, test: 'rspec' })
10
+ # #=> ['bundle', 'gem', 'my_gem', '--exe', '--test=rspec']
11
+ class CommandBuilder
12
+ # @param compatibility_entry [Compatibility::Matrix::Entry]
13
+ def initialize(compatibility_entry:)
14
+ @compatibility_entry = compatibility_entry
15
+ end
16
+
17
+ # Builds the +bundle gem+ command array.
18
+ #
19
+ # @param gem_name [String]
20
+ # @param options [Hash{Symbol => Object}]
21
+ # @return [Array<String>]
22
+ def build(gem_name:, options: {})
23
+ command = ['bundle', 'gem', gem_name]
24
+ Options::Catalog::ORDER.each do |key|
25
+ next unless options.key?(key)
26
+
27
+ append_option!(command, key, options[key])
28
+ end
29
+ command
30
+ end
31
+
32
+ private
33
+
34
+ # @param command [Array<String>]
35
+ # @param key [Symbol]
36
+ # @param value [Object]
37
+ # @return [void]
38
+ def append_option!(command, key, value)
39
+ definition = Options::Catalog.fetch(key)
40
+ case definition[:type]
41
+ when :toggle
42
+ command << definition[:on] if value == true
43
+ command << definition[:off] if value == false
44
+ when :flag
45
+ command << definition[:on] if value == true
46
+ when :enum
47
+ command << "#{definition[:flag]}=#{value}" if value.is_a?(String)
48
+ command << definition[:none] if value == false
49
+ when :string
50
+ command << "#{definition[:flag]}=#{value}" if value.is_a?(String) && !value.empty?
51
+ end
52
+ end
53
+ end
54
+ end