exercism-rb 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 +26 -0
- data/LICENSE +21 -0
- data/README.md +212 -0
- data/bin/xrb +8 -0
- data/lib/exercism/rb/cli.rb +251 -0
- data/lib/exercism/rb/command_runner.rb +29 -0
- data/lib/exercism/rb/config.rb +25 -0
- data/lib/exercism/rb/error.rb +7 -0
- data/lib/exercism/rb/exercise.rb +61 -0
- data/lib/exercism/rb/resolver.rb +55 -0
- data/lib/exercism/rb/state.rb +106 -0
- data/lib/exercism/rb/ui.rb +102 -0
- data/lib/exercism/rb/version.rb +7 -0
- data/lib/exercism/rb.rb +11 -0
- metadata +93 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1e3f086e7d56d3ef1314ac284fd277f39a34b6fba990e3979a20bb1a0a956673
|
|
4
|
+
data.tar.gz: 7258ab70a894221f104f6acb044fdae965d6dcd73af1070e45e829e48e780931
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5ea881fb4335c63357ca7cbe2741c3ecda1d1c12a81363bb2d1354f35db9d6b7cea49fc098da57ccfbb659b164774b52214cbef972b97e80704baa8bb90df0ee
|
|
7
|
+
data.tar.gz: 3537495bab0755c3521642d6014edd7a2f2ae633f8366f700f852bfc89d631495aaf56e3a0f12e874324db0de110c634be4020a86aca3c4dac5f72531d07109b
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## Unreleased
|
|
9
|
+
|
|
10
|
+
## 0.1.0 - 2026-05-06
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `xrb` CLI for downloading, selecting, opening, testing, inspecting, and submitting Exercism Ruby exercises.
|
|
15
|
+
- Source installer for users who want to install directly from the repository.
|
|
16
|
+
- RubyGems release preparation with CI, packaged gem smoke tests, and Trusted Publishing workflow.
|
|
17
|
+
- Colorized CLI output with explicit `XRB_COLOR`, `NO_COLOR`, and `CLICOLOR_FORCE` controls.
|
|
18
|
+
- Project documentation for development, security, contribution, and release practices.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Installation documentation now treats RubyGems as the primary distribution channel.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- State saves now use `Process.pid` for temporary files, avoiding Ruby warnings from an uninitialized global variable.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 hvpaiva
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# exercism-rb
|
|
2
|
+
|
|
3
|
+
`xrb` is a small CLI that removes friction from the Exercism Ruby workflow.
|
|
4
|
+
|
|
5
|
+
It remembers the current exercise, runs commands from the right exercise directory, opens your editor, starts IRB with the solution file loaded, runs tests, and submits the solution file without requiring manual `cd` work.
|
|
6
|
+
|
|
7
|
+
This is an independent helper for the Exercism Ruby track, not an official Exercism project.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
Install from RubyGems:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
gem install exercism-rb
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Make sure your RubyGems executable directory is in your `PATH`, then verify the CLI:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
xrb version
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
To update:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
gem update exercism-rb
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- Ruby 3.2+
|
|
32
|
+
- Exercism CLI for `xrb new` and `xrb submit`
|
|
33
|
+
- An editor available on `PATH`
|
|
34
|
+
|
|
35
|
+
Configure the Exercism CLI separately:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
exercism configure --token=<your-api-token>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Main Flow
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
xrb new assembly-line
|
|
45
|
+
xrb test
|
|
46
|
+
xrb irb
|
|
47
|
+
xrb edit
|
|
48
|
+
xrb submit
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Commands
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
xrb new <exercise> # download, save as current, and open the editor
|
|
55
|
+
xrb edit [exercise] # open the editor for an exercise
|
|
56
|
+
xrb test [exercise] # run the exercise test file with minitest/pride
|
|
57
|
+
xrb irb [exercise] # open irb -r ./<solution>.rb --simple-prompt
|
|
58
|
+
xrb submit [exercise] # submit the solution .rb file
|
|
59
|
+
xrb use <exercise> # save a downloaded exercise as current
|
|
60
|
+
xrb current # show the current exercise
|
|
61
|
+
xrb path [exercise] # print the exercise path
|
|
62
|
+
xrb list # list downloaded exercises
|
|
63
|
+
xrb clear # clear saved state
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Exercise resolution priority:
|
|
67
|
+
|
|
68
|
+
1. Explicit slug, for example `xrb test assembly-line`
|
|
69
|
+
2. Current working directory when inside `XRB_ROOT`
|
|
70
|
+
3. Saved state from the previous `xrb new` or `xrb use`
|
|
71
|
+
|
|
72
|
+
`xrb test` expects a single `*_test.rb` file in the exercise directory and reports an ambiguity if more than one is present.
|
|
73
|
+
|
|
74
|
+
## Output And Color
|
|
75
|
+
|
|
76
|
+
`xrb` uses color automatically when stdout is a terminal. It stays plain when output is redirected, piped, or captured by tests.
|
|
77
|
+
|
|
78
|
+
Color controls:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
XRB_COLOR=auto # default
|
|
82
|
+
XRB_COLOR=always # force ANSI color
|
|
83
|
+
XRB_COLOR=never # disable ANSI color
|
|
84
|
+
NO_COLOR=1 # disable color in auto mode
|
|
85
|
+
CLICOLOR_FORCE=1 # force color in auto mode
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`xrb path` intentionally prints only the resolved path so it can be used in scripts.
|
|
89
|
+
|
|
90
|
+
## State
|
|
91
|
+
|
|
92
|
+
The current exercise is stored as flat TOML:
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
~/.local/state/exercism-rb/state.toml
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Example:
|
|
99
|
+
|
|
100
|
+
```toml
|
|
101
|
+
track = "ruby"
|
|
102
|
+
exercise = "assembly-line"
|
|
103
|
+
path = "/home/hvpaiva/exercism/ruby/assembly-line"
|
|
104
|
+
updated_at = "2026-05-05T12:00:00Z"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Configuration
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
XRB_ROOT=~/exercism/ruby # exercise directory
|
|
111
|
+
XRB_TRACK=ruby # Exercism track
|
|
112
|
+
XRB_EDITOR=nvim # editor used by xrb new/edit
|
|
113
|
+
XRB_STATE=~/.local/state/exercism-rb/state.toml
|
|
114
|
+
XRB_COLOR=auto # auto, always, or never
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`xrb new` uses `exercism download`, which downloads into the workspace configured in the Exercism CLI. If you customize `XRB_ROOT`, configure the Exercism workspace so its track directory matches it, for example `exercism configure --workspace ~/exercism` for `XRB_ROOT=~/exercism/ruby`.
|
|
118
|
+
|
|
119
|
+
Editor commands are split with shell-like quoting, so this works:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
XRB_EDITOR="code --wait" xrb edit
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Source Installer
|
|
126
|
+
|
|
127
|
+
RubyGems is the recommended installation path. The repository also keeps a source installer for users who want to install directly from `main`:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
curl -fsSL https://raw.githubusercontent.com/hvpaiva/exercism-rb/main/install.rb | ruby
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The installer clones or updates the repository at `~/.local/share/exercism-rb` and creates this symlink:
|
|
134
|
+
|
|
135
|
+
```text
|
|
136
|
+
~/.local/bin/xrb -> ~/.local/share/exercism-rb/bin/xrb
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
It also installs the Exercism CLI into `~/.local/bin/exercism` when `exercism` is not already available.
|
|
140
|
+
|
|
141
|
+
To skip the Exercism CLI install:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
curl -fsSL https://raw.githubusercontent.com/hvpaiva/exercism-rb/main/install.rb | ruby - --no-exercism
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
To force-install or update the Exercism CLI:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
curl -fsSL https://raw.githubusercontent.com/hvpaiva/exercism-rb/main/install.rb | ruby - --with-exercism
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
If `~/.local/bin/xrb` is an existing symlink, the installer replaces the symlink without deleting the old target. If it is a real file or directory, the installer refuses to replace it unless you opt in:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
curl -fsSL https://raw.githubusercontent.com/hvpaiva/exercism-rb/main/install.rb | XRB_INSTALL_OVERWRITE=1 ruby
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Development
|
|
160
|
+
|
|
161
|
+
Install development dependencies:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
bundle install
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Run the default test suite:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
bundle exec rake
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Run the full verification suite:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
bundle exec rake ci
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The CI task checks syntax, runs tests, runs tests with Ruby warnings enabled, smoke-tests the checkout executable, builds the gem, installs it into an isolated `GEM_HOME`, and smoke-tests the installed `xrb` executable.
|
|
180
|
+
|
|
181
|
+
Useful individual tasks:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
bundle exec rake test
|
|
185
|
+
bundle exec rake syntax
|
|
186
|
+
bundle exec rake warnings
|
|
187
|
+
bundle exec rake smoke:bin
|
|
188
|
+
bundle exec rake smoke:gem
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Release
|
|
192
|
+
|
|
193
|
+
Publishing is automated through RubyGems Trusted Publishing and GitHub Actions. No long-lived `RUBYGEMS_AUTH_TOKEN` is required for the recommended release path.
|
|
194
|
+
|
|
195
|
+
Before the first release, configure a pending trusted publisher on RubyGems.org:
|
|
196
|
+
|
|
197
|
+
```text
|
|
198
|
+
Gem name: exercism-rb
|
|
199
|
+
GitHub repository: hvpaiva/exercism-rb
|
|
200
|
+
Workflow filename: release.yml
|
|
201
|
+
Environment: release
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Release checklist:
|
|
205
|
+
|
|
206
|
+
1. Update `lib/exercism/rb/version.rb`
|
|
207
|
+
2. Update `CHANGELOG.md`
|
|
208
|
+
3. Run `bundle exec rake ci`
|
|
209
|
+
4. Commit the release changes
|
|
210
|
+
5. Create and push a tag that matches the version, for example `v0.1.0` for `VERSION = "0.1.0"`
|
|
211
|
+
|
|
212
|
+
The `Release` workflow runs only for `v*` tags. It verifies the project and publishes the gem through `rubygems/release-gem@v1`.
|
data/bin/xrb
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
|
|
5
|
+
module Exercism
|
|
6
|
+
module Rb
|
|
7
|
+
class CLI
|
|
8
|
+
COMMANDS = %w[new edit test irb submit use current path list clear help version].freeze
|
|
9
|
+
|
|
10
|
+
def self.start(argv, out: $stdout, err: $stderr)
|
|
11
|
+
new(argv, out: out, err: err).start
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(argv, out: $stdout, err: $stderr)
|
|
15
|
+
@argv = argv.dup
|
|
16
|
+
@ui = UI.new(out: out, err: err)
|
|
17
|
+
@state = State.new
|
|
18
|
+
@runner = CommandRunner.new(ui: @ui)
|
|
19
|
+
@track = Config.track
|
|
20
|
+
@root = Config.root(@track)
|
|
21
|
+
@resolver = Resolver.new(state: @state, track: @track, root: @root)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def start
|
|
25
|
+
command = @argv.shift || "help"
|
|
26
|
+
|
|
27
|
+
case command
|
|
28
|
+
when "new" then new_command
|
|
29
|
+
when "edit" then edit_command
|
|
30
|
+
when "test" then test_command
|
|
31
|
+
when "irb" then irb_command
|
|
32
|
+
when "submit" then submit_command
|
|
33
|
+
when "use" then use_command
|
|
34
|
+
when "current" then current_command
|
|
35
|
+
when "path" then path_command
|
|
36
|
+
when "list" then list_command
|
|
37
|
+
when "clear" then clear_command
|
|
38
|
+
when "help", "-h", "--help" then help_command
|
|
39
|
+
when "version", "-v", "--version" then version_command
|
|
40
|
+
else
|
|
41
|
+
raise Error, "Unknown command: #{command}. Use `xrb help`."
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
0
|
|
45
|
+
rescue Error => e
|
|
46
|
+
@ui.error(e.message)
|
|
47
|
+
1
|
|
48
|
+
rescue Interrupt
|
|
49
|
+
@ui.error("Interrupted.")
|
|
50
|
+
130
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def new_command
|
|
56
|
+
exercise = exercise_from_required_arg("new <exercise>", require_existing: false)
|
|
57
|
+
|
|
58
|
+
@ui.info("Downloading #{@ui.highlight(exercise.slug)}...")
|
|
59
|
+
@runner.run("exercism", "download", "--track=#{exercise.track}", "--exercise=#{exercise.slug}")
|
|
60
|
+
exercise.ensure_exists!
|
|
61
|
+
save_current(exercise)
|
|
62
|
+
@ui.key_value("Path", @ui.path(exercise.path))
|
|
63
|
+
edit_exercise(exercise)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def edit_command
|
|
67
|
+
edit_exercise(@resolver.resolve(optional_arg))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_command
|
|
71
|
+
exercise = @resolver.resolve(optional_arg)
|
|
72
|
+
test_file = exercise.test_file
|
|
73
|
+
|
|
74
|
+
@ui.info("Testing #{@ui.highlight(exercise.slug)}...")
|
|
75
|
+
@runner.run("ruby", "-r", "minitest/pride", test_file, chdir: exercise.path)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def irb_command
|
|
79
|
+
exercise = @resolver.resolve(optional_arg)
|
|
80
|
+
solution_file = exercise.solution_file
|
|
81
|
+
|
|
82
|
+
@ui.info("Opening IRB for #{@ui.highlight(exercise.slug)}...")
|
|
83
|
+
@runner.run("irb", "-r", "./#{solution_file}", "--simple-prompt", chdir: exercise.path)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def submit_command
|
|
87
|
+
exercise = @resolver.resolve(optional_arg)
|
|
88
|
+
solution_file = exercise.solution_file
|
|
89
|
+
|
|
90
|
+
@ui.info("Submitting #{@ui.highlight(exercise.slug)}...")
|
|
91
|
+
@runner.run("exercism", "submit", solution_file, chdir: exercise.path)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def use_command
|
|
95
|
+
exercise = exercise_from_required_arg("use <exercise>")
|
|
96
|
+
save_current(exercise)
|
|
97
|
+
@ui.success("Current exercise: #{@ui.highlight(exercise.slug)}")
|
|
98
|
+
@ui.command(@ui.path(exercise.path))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def current_command
|
|
102
|
+
data = @state.load
|
|
103
|
+
raise Error, "No current exercise saved." if data.empty? || blank?(data["exercise"])
|
|
104
|
+
|
|
105
|
+
path = data["path"]
|
|
106
|
+
@ui.title("Current exercise")
|
|
107
|
+
@ui.key_value("Exercise", @ui.highlight(data.fetch("exercise")))
|
|
108
|
+
@ui.key_value("Track", data.fetch("track", @track))
|
|
109
|
+
@ui.key_value("Path", @ui.path(path))
|
|
110
|
+
@ui.key_value("State", @ui.path(@state.path))
|
|
111
|
+
@ui.warn("The saved directory no longer exists.") if path && !Dir.exist?(path)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def path_command
|
|
115
|
+
exercise = @resolver.resolve(optional_arg)
|
|
116
|
+
@ui.say(exercise.path)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def list_command
|
|
120
|
+
raise Error, "Exercise directory not found: #{@root}" unless Dir.exist?(@root)
|
|
121
|
+
|
|
122
|
+
current = @state.load["exercise"]
|
|
123
|
+
exercises = Dir.children(@root)
|
|
124
|
+
.select { |name| File.directory?(File.join(@root, name)) && !name.start_with?(".") }
|
|
125
|
+
.sort
|
|
126
|
+
|
|
127
|
+
if exercises.empty?
|
|
128
|
+
@ui.warn("No exercises downloaded in #{@root}.")
|
|
129
|
+
return
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
@ui.section("Downloaded exercises")
|
|
133
|
+
exercises.each do |slug|
|
|
134
|
+
marker = slug == current ? "*" : " "
|
|
135
|
+
label = slug == current ? @ui.highlight(slug) : slug
|
|
136
|
+
suffix = slug == current ? " #{@ui.muted('current')}" : ""
|
|
137
|
+
@ui.say("#{marker} #{label}#{suffix}")
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def clear_command
|
|
142
|
+
@state.clear
|
|
143
|
+
@ui.success("State cleared: #{@state.path}")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def help_command
|
|
147
|
+
@ui.say(<<~HELP)
|
|
148
|
+
#{@ui.bold('xrb')} - Exercism Ruby helper
|
|
149
|
+
|
|
150
|
+
Usage:
|
|
151
|
+
xrb new <exercise> download, save as current, and open the editor
|
|
152
|
+
xrb edit [exercise] open the editor for an exercise
|
|
153
|
+
xrb test [exercise] run the exercise test file with minitest/pride
|
|
154
|
+
xrb irb [exercise] open irb -r ./<solution>.rb --simple-prompt
|
|
155
|
+
xrb submit [exercise] submit the solution .rb file
|
|
156
|
+
xrb use <exercise> save a downloaded exercise as current
|
|
157
|
+
xrb current show the current exercise
|
|
158
|
+
xrb path [exercise] print the exercise path
|
|
159
|
+
xrb list list downloaded exercises
|
|
160
|
+
xrb clear clear saved state
|
|
161
|
+
|
|
162
|
+
State:
|
|
163
|
+
#{@state.path}
|
|
164
|
+
|
|
165
|
+
Environment:
|
|
166
|
+
XRB_ROOT exercise directory (current: #{@root})
|
|
167
|
+
XRB_TRACK Exercism track (current: #{@track})
|
|
168
|
+
XRB_EDITOR editor used by xrb edit/new (default: nvim)
|
|
169
|
+
XRB_STATE TOML state file
|
|
170
|
+
XRB_COLOR color output: auto, always, or never
|
|
171
|
+
HELP
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def version_command
|
|
175
|
+
@ui.say("xrb #{VERSION}")
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def edit_exercise(exercise)
|
|
179
|
+
target = editable_target(exercise)
|
|
180
|
+
editor_args = editor_args_from_config
|
|
181
|
+
raise Error, "Invalid editor in XRB_EDITOR/VISUAL/EDITOR." if editor_args.empty?
|
|
182
|
+
ensure_editor_available!(editor_args.first, chdir: exercise.path)
|
|
183
|
+
|
|
184
|
+
@ui.info("Opening #{@ui.highlight(exercise.slug)}...")
|
|
185
|
+
@runner.run(*editor_args, target, chdir: exercise.path)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def editor_args_from_config
|
|
189
|
+
Shellwords.split(Config.editor)
|
|
190
|
+
rescue ArgumentError => e
|
|
191
|
+
raise Error, "Invalid editor in XRB_EDITOR/VISUAL/EDITOR: #{e.message}"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def ensure_editor_available!(command, chdir:)
|
|
195
|
+
return if editor_command_available?(command, chdir: chdir)
|
|
196
|
+
|
|
197
|
+
raise Error, "Editor not found: #{command}. Set XRB_EDITOR, VISUAL, or EDITOR to an installed executable."
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def editor_command_available?(command, chdir:)
|
|
201
|
+
if command_path?(command)
|
|
202
|
+
return executable_file?(File.absolute_path(command, chdir))
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? do |dir|
|
|
206
|
+
candidate_dir = dir.empty? ? "." : dir
|
|
207
|
+
executable_file?(File.absolute_path(File.join(candidate_dir, command), chdir))
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def command_path?(command)
|
|
212
|
+
command.include?(File::SEPARATOR) || (File::ALT_SEPARATOR && command.include?(File::ALT_SEPARATOR))
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def executable_file?(path)
|
|
216
|
+
File.file?(path) && File.executable?(path)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def editable_target(exercise)
|
|
220
|
+
exercise.solution_file
|
|
221
|
+
rescue Error
|
|
222
|
+
"."
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def save_current(exercise)
|
|
226
|
+
@state.save(track: exercise.track, exercise: exercise.slug, path: exercise.path)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def exercise_from_required_arg(usage, require_existing: true)
|
|
230
|
+
slug = @argv.shift
|
|
231
|
+
raise Error, "Usage: xrb #{usage}" if blank?(slug)
|
|
232
|
+
raise Error, "Too many arguments: #{@argv.join(' ')}" unless @argv.empty?
|
|
233
|
+
|
|
234
|
+
Exercise.new(slug: slug, track: @track, root: @root).tap do |exercise|
|
|
235
|
+
exercise.ensure_exists! if require_existing
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def optional_arg
|
|
240
|
+
slug = @argv.shift
|
|
241
|
+
raise Error, "Too many arguments: #{@argv.join(' ')}" unless @argv.empty?
|
|
242
|
+
|
|
243
|
+
slug
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def blank?(value)
|
|
247
|
+
value.nil? || value.to_s.strip.empty?
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
|
|
5
|
+
module Exercism
|
|
6
|
+
module Rb
|
|
7
|
+
class CommandRunner
|
|
8
|
+
def initialize(ui: UI.new)
|
|
9
|
+
@ui = ui
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def run(*args, chdir: nil)
|
|
13
|
+
printable = Shellwords.join(args)
|
|
14
|
+
printable = "cd #{Shellwords.escape(chdir)} && #{printable}" if chdir
|
|
15
|
+
@ui.command("$ #{printable}")
|
|
16
|
+
|
|
17
|
+
ok = if chdir
|
|
18
|
+
Dir.chdir(chdir) { system(*args) }
|
|
19
|
+
else
|
|
20
|
+
system(*args)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
raise Error, "Command failed: #{Shellwords.join(args)}" unless ok
|
|
24
|
+
|
|
25
|
+
true
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Exercism
|
|
4
|
+
module Rb
|
|
5
|
+
module Config
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def track
|
|
9
|
+
ENV.fetch("XRB_TRACK", "ruby")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def root(track_name = track)
|
|
13
|
+
File.expand_path(ENV.fetch("XRB_ROOT", File.join(Dir.home, "exercism", track_name)))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def state_path
|
|
17
|
+
File.expand_path(ENV.fetch("XRB_STATE", File.join(Dir.home, ".local", "state", "exercism-rb", "state.toml")))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def editor
|
|
21
|
+
ENV["XRB_EDITOR"] || ENV["VISUAL"] || ENV["EDITOR"] || "nvim"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Exercism
|
|
4
|
+
module Rb
|
|
5
|
+
class Exercise
|
|
6
|
+
SLUG_PATTERN = /\A[a-z0-9][a-z0-9-]*\z/
|
|
7
|
+
|
|
8
|
+
attr_reader :slug, :track, :root, :path
|
|
9
|
+
|
|
10
|
+
def initialize(slug:, track: Config.track, root: Config.root(track), path: nil)
|
|
11
|
+
slug = slug.to_s.strip
|
|
12
|
+
raise Error, "Exercise is required." if slug.empty?
|
|
13
|
+
raise Error, "Invalid slug: #{slug.inspect}. Use something like assembly-line." unless slug.match?(SLUG_PATTERN)
|
|
14
|
+
|
|
15
|
+
@slug = slug
|
|
16
|
+
@track = track
|
|
17
|
+
@root = File.expand_path(root)
|
|
18
|
+
@path = File.expand_path(path || File.join(@root, @slug))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def exists?
|
|
22
|
+
Dir.exist?(@path)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def ensure_exists!
|
|
26
|
+
return self if exists?
|
|
27
|
+
|
|
28
|
+
raise Error, "Exercise not found: #{@path}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_file
|
|
32
|
+
ensure_exists!
|
|
33
|
+
pick_one(files_matching { |name| name.end_with?("_test.rb") }, kind: "test file (*_test.rb)")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def solution_file
|
|
37
|
+
ensure_exists!
|
|
38
|
+
pick_one(files_matching { |name| name.end_with?(".rb") && !name.end_with?("_test.rb") }, kind: "solution file (.rb)")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def files_matching
|
|
44
|
+
Dir.children(@path)
|
|
45
|
+
.select { |name| File.file?(File.join(@path, name)) && yield(name) }
|
|
46
|
+
.sort
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def pick_one(files, kind:)
|
|
50
|
+
case files.length
|
|
51
|
+
when 0
|
|
52
|
+
raise Error, "Could not find #{kind} in #{@path}"
|
|
53
|
+
when 1
|
|
54
|
+
files.first
|
|
55
|
+
else
|
|
56
|
+
raise Error, "Found more than one #{kind} in #{@path}: #{files.join(', ')}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Exercism
|
|
4
|
+
module Rb
|
|
5
|
+
class Resolver
|
|
6
|
+
def initialize(state:, track: Config.track, root: Config.root(track))
|
|
7
|
+
@state = state
|
|
8
|
+
@track = track
|
|
9
|
+
@root = File.expand_path(root)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def resolve(slug = nil, require_existing: true)
|
|
13
|
+
exercise = if present?(slug)
|
|
14
|
+
Exercise.new(slug: slug, track: @track, root: @root)
|
|
15
|
+
else
|
|
16
|
+
from_current_directory || from_state
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
exercise.ensure_exists! if require_existing
|
|
20
|
+
exercise
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def from_current_directory
|
|
26
|
+
cwd = File.expand_path(Dir.pwd)
|
|
27
|
+
root_with_separator = @root.end_with?(File::SEPARATOR) ? @root : "#{@root}#{File::SEPARATOR}"
|
|
28
|
+
return nil unless cwd.start_with?(root_with_separator)
|
|
29
|
+
|
|
30
|
+
relative = cwd.delete_prefix(root_with_separator)
|
|
31
|
+
slug = relative.split(File::SEPARATOR).first
|
|
32
|
+
return nil unless present?(slug)
|
|
33
|
+
|
|
34
|
+
Exercise.new(slug: slug, track: @track, root: @root)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def from_state
|
|
38
|
+
data = @state.load
|
|
39
|
+
slug = data["exercise"]
|
|
40
|
+
raise Error, "No current exercise. Use `xrb new <exercise>` or `xrb use <exercise>`." unless present?(slug)
|
|
41
|
+
|
|
42
|
+
Exercise.new(
|
|
43
|
+
slug: slug,
|
|
44
|
+
track: data["track"] || @track,
|
|
45
|
+
root: File.dirname(data["path"] || File.join(@root, slug)),
|
|
46
|
+
path: data["path"]
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def present?(value)
|
|
51
|
+
!value.nil? && !value.to_s.strip.empty?
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Exercism
|
|
7
|
+
module Rb
|
|
8
|
+
class State
|
|
9
|
+
KEYS = %w[track exercise path updated_at].freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :path
|
|
12
|
+
|
|
13
|
+
def initialize(path: Config.state_path)
|
|
14
|
+
@path = File.expand_path(path)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def load
|
|
18
|
+
return {} unless File.file?(@path)
|
|
19
|
+
|
|
20
|
+
parse(File.read(@path))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def save(track:, exercise:, path:)
|
|
24
|
+
data = {
|
|
25
|
+
"track" => track,
|
|
26
|
+
"exercise" => exercise,
|
|
27
|
+
"path" => File.expand_path(path),
|
|
28
|
+
"updated_at" => Time.now.utc.iso8601
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
32
|
+
tmp_path = "#{@path}.tmp.#{Process.pid}"
|
|
33
|
+
File.write(tmp_path, to_toml(data))
|
|
34
|
+
File.rename(tmp_path, @path)
|
|
35
|
+
data
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def clear
|
|
39
|
+
File.delete(@path) if File.file?(@path)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def parse(content)
|
|
45
|
+
data = {}
|
|
46
|
+
|
|
47
|
+
content.each_line.with_index(1) do |line, number|
|
|
48
|
+
stripped = line.strip
|
|
49
|
+
next if stripped.empty? || stripped.start_with?("#")
|
|
50
|
+
|
|
51
|
+
match = stripped.match(/\A([A-Za-z0-9_-]+)\s*=\s*(.+)\z/)
|
|
52
|
+
raise Error, "Invalid state in #{@path}: line #{number}" unless match
|
|
53
|
+
|
|
54
|
+
key = match[1]
|
|
55
|
+
next unless KEYS.include?(key)
|
|
56
|
+
|
|
57
|
+
data[key] = parse_value(match[2].strip)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
data
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def parse_value(raw)
|
|
64
|
+
quoted_start = raw.start_with?("\"")
|
|
65
|
+
quoted_end = raw.end_with?("\"")
|
|
66
|
+
raise Error, "Invalid quoted value in #{@path}" if quoted_start != quoted_end
|
|
67
|
+
|
|
68
|
+
return unquote(raw) if raw.start_with?("\"") && raw.end_with?("\"")
|
|
69
|
+
|
|
70
|
+
raw
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def unquote(raw)
|
|
74
|
+
value = raw[1...-1]
|
|
75
|
+
value.gsub(/\\(["\\nrt])/) do
|
|
76
|
+
case Regexp.last_match(1)
|
|
77
|
+
when '"' then '"'
|
|
78
|
+
when "\\" then "\\"
|
|
79
|
+
when "n" then "\n"
|
|
80
|
+
when "r" then "\r"
|
|
81
|
+
when "t" then "\t"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def to_toml(data)
|
|
87
|
+
KEYS.filter_map do |key|
|
|
88
|
+
next unless data.key?(key)
|
|
89
|
+
|
|
90
|
+
"#{key} = #{quote(data.fetch(key))}"
|
|
91
|
+
end.join("\n") + "\n"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def quote(value)
|
|
95
|
+
escaped = value.to_s
|
|
96
|
+
.gsub("\\", "\\\\")
|
|
97
|
+
.gsub('"', '\\"')
|
|
98
|
+
.gsub("\n", "\\n")
|
|
99
|
+
.gsub("\r", "\\r")
|
|
100
|
+
.gsub("\t", "\\t")
|
|
101
|
+
|
|
102
|
+
%("#{escaped}")
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Exercism
|
|
4
|
+
module Rb
|
|
5
|
+
class UI
|
|
6
|
+
COLORS = {
|
|
7
|
+
blue: "\e[34m",
|
|
8
|
+
cyan: "\e[36m",
|
|
9
|
+
green: "\e[32m",
|
|
10
|
+
red: "\e[31m",
|
|
11
|
+
yellow: "\e[33m",
|
|
12
|
+
gray: "\e[90m",
|
|
13
|
+
bold: "\e[1m",
|
|
14
|
+
reset: "\e[0m"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def initialize(out: $stdout, err: $stderr, color: nil)
|
|
18
|
+
@out = out
|
|
19
|
+
@err = err
|
|
20
|
+
@color = color.nil? ? default_color? : color
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def say(message = "")
|
|
24
|
+
@out.puts(message)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def title(message)
|
|
28
|
+
@out.puts(bold(message))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def section(message)
|
|
32
|
+
@out.puts(paint(message, :cyan))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def key_value(key, value, width: 8)
|
|
36
|
+
label = key.to_s.ljust(width)
|
|
37
|
+
@out.puts("#{paint(label, :gray)} #{value}")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def path(value)
|
|
41
|
+
paint(value, :blue)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def highlight(value)
|
|
45
|
+
paint(value, :bold)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def muted(value)
|
|
49
|
+
paint(value, :gray)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def info(message)
|
|
53
|
+
@out.puts("#{paint('info', :blue)} #{message}")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def success(message)
|
|
57
|
+
@out.puts("#{paint('done', :green)} #{message}")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def warn(message)
|
|
61
|
+
@err.puts("#{paint('warn', :yellow)} #{message}")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def error(message)
|
|
65
|
+
@err.puts("#{paint('error', :red)} #{message}")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def command(message)
|
|
69
|
+
@out.puts(paint(message, :gray))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def bold(message)
|
|
73
|
+
paint(message, :bold)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def default_color?
|
|
79
|
+
color_mode = ENV.fetch("XRB_COLOR", "").strip.downcase
|
|
80
|
+
|
|
81
|
+
return true if %w[always force true yes 1].include?(color_mode)
|
|
82
|
+
return false if %w[never none false no 0].include?(color_mode)
|
|
83
|
+
return false if ENV.key?("NO_COLOR")
|
|
84
|
+
return true if force_color?
|
|
85
|
+
return false if ENV.fetch("CLICOLOR", nil) == "0"
|
|
86
|
+
|
|
87
|
+
@out.tty?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def paint(message, color)
|
|
91
|
+
return message unless @color
|
|
92
|
+
|
|
93
|
+
"#{COLORS.fetch(color)}#{message}#{COLORS.fetch(:reset)}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def force_color?
|
|
97
|
+
value = ENV.fetch("CLICOLOR_FORCE", nil)
|
|
98
|
+
!value.nil? && !value.empty? && value != "0"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
data/lib/exercism/rb.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "rb/version"
|
|
4
|
+
require_relative "rb/config"
|
|
5
|
+
require_relative "rb/error"
|
|
6
|
+
require_relative "rb/ui"
|
|
7
|
+
require_relative "rb/state"
|
|
8
|
+
require_relative "rb/exercise"
|
|
9
|
+
require_relative "rb/resolver"
|
|
10
|
+
require_relative "rb/command_runner"
|
|
11
|
+
require_relative "rb/cli"
|
metadata
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: exercism-rb
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- hvpaiva
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-05-06 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: minitest
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '5.25'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '5.25'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '13.2'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '13.2'
|
|
41
|
+
description: A small command-line helper that keeps Exercism Ruby exercises easy to
|
|
42
|
+
open, test, inspect, and submit.
|
|
43
|
+
email:
|
|
44
|
+
- hvpaiva@users.noreply.github.com
|
|
45
|
+
executables:
|
|
46
|
+
- xrb
|
|
47
|
+
extensions: []
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- CHANGELOG.md
|
|
51
|
+
- LICENSE
|
|
52
|
+
- README.md
|
|
53
|
+
- bin/xrb
|
|
54
|
+
- lib/exercism/rb.rb
|
|
55
|
+
- lib/exercism/rb/cli.rb
|
|
56
|
+
- lib/exercism/rb/command_runner.rb
|
|
57
|
+
- lib/exercism/rb/config.rb
|
|
58
|
+
- lib/exercism/rb/error.rb
|
|
59
|
+
- lib/exercism/rb/exercise.rb
|
|
60
|
+
- lib/exercism/rb/resolver.rb
|
|
61
|
+
- lib/exercism/rb/state.rb
|
|
62
|
+
- lib/exercism/rb/ui.rb
|
|
63
|
+
- lib/exercism/rb/version.rb
|
|
64
|
+
homepage: https://github.com/hvpaiva/exercism-rb
|
|
65
|
+
licenses:
|
|
66
|
+
- MIT
|
|
67
|
+
metadata:
|
|
68
|
+
allowed_push_host: https://rubygems.org
|
|
69
|
+
bug_tracker_uri: https://github.com/hvpaiva/exercism-rb/issues
|
|
70
|
+
changelog_uri: https://github.com/hvpaiva/exercism-rb/blob/main/CHANGELOG.md
|
|
71
|
+
homepage_uri: https://github.com/hvpaiva/exercism-rb
|
|
72
|
+
rubygems_mfa_required: 'true'
|
|
73
|
+
source_code_uri: https://github.com/hvpaiva/exercism-rb
|
|
74
|
+
post_install_message:
|
|
75
|
+
rdoc_options: []
|
|
76
|
+
require_paths:
|
|
77
|
+
- lib
|
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: 3.2.0
|
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '0'
|
|
88
|
+
requirements: []
|
|
89
|
+
rubygems_version: 3.4.19
|
|
90
|
+
signing_key:
|
|
91
|
+
specification_version: 4
|
|
92
|
+
summary: Exercism Ruby workflow helper CLI
|
|
93
|
+
test_files: []
|