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 +7 -0
- data/CHANGELOG.md +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +150 -0
- data/exe/create-ruby-gem +13 -0
- data/lib/create_ruby_gem/cli.rb +360 -0
- data/lib/create_ruby_gem/command_builder.rb +54 -0
- data/lib/create_ruby_gem/compatibility/matrix.rb +118 -0
- data/lib/create_ruby_gem/config/store.rb +125 -0
- data/lib/create_ruby_gem/detection/bundler_defaults.rb +88 -0
- data/lib/create_ruby_gem/detection/bundler_version.rb +31 -0
- data/lib/create_ruby_gem/detection/runtime.rb +33 -0
- data/lib/create_ruby_gem/error.rb +15 -0
- data/lib/create_ruby_gem/options/catalog.rb +49 -0
- data/lib/create_ruby_gem/options/validator.rb +102 -0
- data/lib/create_ruby_gem/runner.rb +39 -0
- data/lib/create_ruby_gem/ui/back_navigation_patch.rb +41 -0
- data/lib/create_ruby_gem/ui/palette.rb +73 -0
- data/lib/create_ruby_gem/ui/prompter.rb +77 -0
- data/lib/create_ruby_gem/version.rb +6 -0
- data/lib/create_ruby_gem/wizard.rb +339 -0
- data/lib/create_ruby_gem.rb +29 -0
- metadata +96 -0
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 [](https://rubygems.org/gems/create-ruby-gem) [](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).
|
data/exe/create-ruby-gem
ADDED
|
@@ -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
|