gempilot 0.3.1 → 0.3.3
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 +4 -4
- data/.ruby-version +1 -1
- data/data/templates/gem/Gemfile.erb +1 -0
- data/docs/superpowers/plans/2026-09-23-command-generation-bootstrap.md +810 -0
- data/docs/superpowers/plans/2026-09-23-land-betterleaks-jruby.md +262 -0
- data/docs/superpowers/plans/2026-09-23-multi-gem-rubygems-push.md +318 -0
- data/docs/superpowers/plans/2026-09-23-zeitwerk-task-inflector.md +911 -0
- data/issues.rec +18 -1
- data/lib/gempilot/cli/command.rb +1 -1
- data/lib/gempilot/version.rb +1 -1
- data/vendor/vendored.gemv +0 -0
- metadata +6 -2
|
@@ -0,0 +1,810 @@
|
|
|
1
|
+
# Command Generation Bootstrap Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Make `gempilot new command NAME` produce a command that actually runs: bootstrap the CommandKit plumbing the gem is missing (CLI router, base command, an executable that starts the router and is `chmod +x`, the `command_kit` runtime dependency followed by `bundle install`, and the Zeitwerk inflection that lets `cli.rb` define `CLI`), and give `gempilot create --exe` the same scaffold so a CLI gem works from its first commit.
|
|
6
|
+
|
|
7
|
+
**Architecture:** A new `Gempilot::CLI::CliBootstrap` mixin, shared by the `New` and `Create` commands, owns the scaffold. Every step is idempotent (it checks for the file, the dependency line, or the inflection before acting), so `new command` on an already-bootstrapped gem touches nothing, and running it twice is safe. Templates live in a new `data/templates/cli/` directory; `Generator#erb` gains a `from:` keyword so both commands can render them regardless of their own `template_dir`. `create --exe` now runs the bootstrap from inside the new gem instead of rendering the old bare executable, and the generated command test asserts something real (`command_name`) instead of `assert command`, which the generated gem's own RuboCop rejects.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Ruby 4.0, command_kit 0.6 (`CommandKit::Commands`, `Commands::AutoLoad`, `Options::Version`, `Command#command_name`), Zeitwerk (`inflector.inflect("cli" => "CLI")`), ERB, Minitest (`test/`), RSpec (`spec/`), RuboCop with rubocop-claude.
|
|
10
|
+
|
|
11
|
+
**Issue:** `69816386-9F7F-11F1-8629-FE6CB9572C2F` — "Command generation is incomplete".
|
|
12
|
+
|
|
13
|
+
## Global Constraints
|
|
14
|
+
|
|
15
|
+
- Ruby `>= 4.0`; `it` block parameter is fine; `File.absolute_path?` etc. available.
|
|
16
|
+
- Double-quoted strings (`Style/StringLiterals: double_quotes`, single quotes allowed only when the string itself contains double quotes); NO `# frozen_string_literal:` comments; trailing commas in multiline literals/arguments; string constants end in `.freeze`.
|
|
17
|
+
- rdoc doc blocks in canonical form (a bare `##` line, then `# ...` lines); never a comment line that reads like code (`Claude/NoCommentedCode`, `MinLines: 1`); no `TODO`/`NOTE` in comments without attribution (`Claude/TaggedComments`) — `TODO` inside string literals is fine.
|
|
18
|
+
- Method bodies max 10 lines, ABC max 17, `Metrics/BlockLength` max 8 (heredoc/array/hash/method_call count as one); `Naming/PredicateMethod` forbids a method that only returns boolean literals unless it ends in `?` (which is why `insert_dependency` returns the path, not `true`).
|
|
19
|
+
- Zeitwerk: `lib/gempilot/cli/cli_bootstrap.rb` MUST define `Gempilot::CLI::CliBootstrap` (`spec/zeitwerk_spec.rb` eager-loads gempilot; the `cli` directory inflects to `CLI`, the file name to `CliBootstrap`).
|
|
20
|
+
- Generated files must pass the generated gem's own RuboCop (rubocop-claude included) and its Zeitwerk eager-load test; `Style/Documentation` needs a doc block on `class Deploy < Command`, `Style/ClassAndModuleChildren` is disabled in generated gems so `module My::Gem` compact style is fine.
|
|
21
|
+
- The generated gem's `.rubocop.yml` excludes `bin/*` but not `exe/`, so the executable is linted; its content mirrors gempilot's own `exe/gempilot`, which passes the same config.
|
|
22
|
+
- Verification: `bundle exec ruby -Itest -Ilib test/gempilot/cli/new_command_test.rb` (fast), `bundle exec ruby -Itest -Ilib test/gempilot/cli/create_command_test.rb` (includes integration tests that run `bundle exec rake` inside generated gems, ~2 minutes), `bundle exec rspec`, `bundle exec rubocop <files>`, and `bundle exec rake default` for the full gate.
|
|
23
|
+
- Baseline (verified 2026-09-23): minitest `109 runs, 4 failures` — the 4 are the Gemfile-template ordering regression fixed by Task 1 of `docs/superpowers/plans/2026-09-23-land-betterleaks-jruby.md`; apply that one-line change first, after which the baseline is `109 runs, 0 failures`, RSpec `192 examples, 0 failures`, RuboCop no offenses. Final state after this plan: minitest `128 runs, 0 failures`, RSpec `192 examples`, RuboCop no offenses.
|
|
24
|
+
- Commit messages: plain imperative sentences, no conventional-commit prefixes.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
### Task 1: Shared generator helpers
|
|
29
|
+
|
|
30
|
+
**Files:**
|
|
31
|
+
- Modify: `lib/gempilot/cli/generator.rb` (add `update_file`, `ensure_directory`; extend `erb`)
|
|
32
|
+
- Modify: `lib/gempilot/cli/commands/new.rb` (delete its private `ensure_directory`)
|
|
33
|
+
- Create: `data/templates/cli/exe.erb` (needed by the new generator test; the other two CLI templates arrive in Task 2)
|
|
34
|
+
- Test: `test/gempilot/cli/generator_test.rb`
|
|
35
|
+
|
|
36
|
+
**Interfaces:**
|
|
37
|
+
- Consumes: `CommandKit::FileUtils#erb(source, dest = nil)` (super), `Generator#print_action`, `Generator#mkdir`.
|
|
38
|
+
- Produces: `Generator#update_file(path, content)` (prints `update`, writes), `Generator#ensure_directory(dir)` (prints `mkdir` only when creating), `Generator#erb(source, dest = nil, from: @template_dir)` (renders `File.join(from, source)`). Tasks 2–3 rely on all three.
|
|
39
|
+
|
|
40
|
+
- [ ] **Step 1: Write the failing tests**
|
|
41
|
+
|
|
42
|
+
In `test/gempilot/cli/generator_test.rb`, immediately before `def test_create_file_writes_content`, add:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
def test_update_file_overwrites_content
|
|
46
|
+
path = File.join(@tmpdir, "existing.rb")
|
|
47
|
+
File.write(path, "old\n")
|
|
48
|
+
@generator.update_file(path, "new\n")
|
|
49
|
+
|
|
50
|
+
assert_equal "new\n", File.read(path)
|
|
51
|
+
assert_includes @stdout.string, "update"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_ensure_directory_creates_a_missing_directory_once
|
|
55
|
+
path = File.join(@tmpdir, "nested")
|
|
56
|
+
@generator.ensure_directory(path)
|
|
57
|
+
@generator.ensure_directory(path)
|
|
58
|
+
|
|
59
|
+
assert_predicate Pathname(path), :directory?
|
|
60
|
+
assert_equal 1, @stdout.string.scan("mkdir").size
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_erb_renders_from_another_template_directory
|
|
64
|
+
@generator.instance_variable_set(:@require_path, "test_gem")
|
|
65
|
+
@generator.instance_variable_set(:@gem_module, "TestGem")
|
|
66
|
+
dest = File.join(@tmpdir, "exe")
|
|
67
|
+
@generator.erb("exe.erb", dest, from: File.join(Gempilot::ROOT, "data", "templates", "cli"))
|
|
68
|
+
|
|
69
|
+
assert_includes File.read(dest), "TestGem::CLI.start"
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- [ ] **Step 2: Run them to verify they fail**
|
|
74
|
+
|
|
75
|
+
Run: `bundle exec ruby -Itest -Ilib test/gempilot/cli/generator_test.rb`
|
|
76
|
+
Expected: `12 runs, ... 3 errors` — `NoMethodError: undefined method 'update_file'`, `undefined method 'ensure_directory'`, and `ArgumentError: unknown keyword: :from`.
|
|
77
|
+
|
|
78
|
+
- [ ] **Step 3: Create the executable template**
|
|
79
|
+
|
|
80
|
+
Create `data/templates/cli/exe.erb` (this is gempilot's own `exe/gempilot`, parameterised):
|
|
81
|
+
|
|
82
|
+
```erb
|
|
83
|
+
#!/usr/bin/env ruby
|
|
84
|
+
|
|
85
|
+
gemfile = File.expand_path("../Gemfile", __dir__)
|
|
86
|
+
|
|
87
|
+
if File.exist?(gemfile)
|
|
88
|
+
ENV["BUNDLE_GEMFILE"] ||= gemfile
|
|
89
|
+
require "bundler/setup"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
require "<%= @require_path %>/cli"
|
|
93
|
+
|
|
94
|
+
<%= @gem_module %>::CLI.start
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
- [ ] **Step 4: Extend Generator**
|
|
98
|
+
|
|
99
|
+
In `lib/gempilot/cli/generator.rb`, directly after `create_file`, add:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
def update_file(path, content)
|
|
103
|
+
print_action "update", path
|
|
104
|
+
File.write(path, content)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def ensure_directory(dir)
|
|
108
|
+
mkdir(dir) unless File.directory?(dir)
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
and replace the existing `erb` method with:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
# Renders +source+ from the command's template directory, or from the
|
|
116
|
+
# directory given as +from+ for templates shared across commands.
|
|
117
|
+
def erb(source, dest = nil, from: @template_dir)
|
|
118
|
+
print_action("erb", dest, source: source) if dest
|
|
119
|
+
|
|
120
|
+
super(File.join(from, source), dest)
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
In `lib/gempilot/cli/commands/new.rb`, delete the now-duplicated private method:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
def ensure_directory(dir)
|
|
128
|
+
mkdir(dir) unless File.directory?(dir)
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
(`New`'s other methods keep calling `ensure_directory`; it now comes from `Generator`.)
|
|
133
|
+
|
|
134
|
+
- [ ] **Step 5: Run the tests and rubocop**
|
|
135
|
+
|
|
136
|
+
Run: `bundle exec ruby -Itest -Ilib test/gempilot/cli/generator_test.rb && bundle exec ruby -Itest -Ilib test/gempilot/cli/new_command_test.rb && bundle exec rubocop lib/gempilot/cli/generator.rb lib/gempilot/cli/commands/new.rb test/gempilot/cli/generator_test.rb`
|
|
137
|
+
Expected: `12 runs, 0 failures, 0 errors`; `19 runs, 0 failures`; `3 files inspected, no offenses detected`.
|
|
138
|
+
|
|
139
|
+
- [ ] **Step 6: Commit**
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
git add lib/gempilot/cli/generator.rb lib/gempilot/cli/commands/new.rb data/templates/cli/exe.erb test/gempilot/cli/generator_test.rb
|
|
143
|
+
git commit -m "Add shared generator helpers for templates rendered by several commands"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
### Task 2: `CliBootstrap` and `gempilot new command`
|
|
149
|
+
|
|
150
|
+
**Files:**
|
|
151
|
+
- Create: `lib/gempilot/cli/cli_bootstrap.rb`
|
|
152
|
+
- Create: `data/templates/cli/cli.rb.erb`, `data/templates/cli/command.rb.erb`
|
|
153
|
+
- Modify: `data/templates/new/command.rb.erb`
|
|
154
|
+
- Modify: `lib/gempilot/cli/commands/new.rb` (`include CliBootstrap`; `add_command` and the generated test content)
|
|
155
|
+
- Test: `test/gempilot/cli/new_command_test.rb`
|
|
156
|
+
|
|
157
|
+
**Interfaces:**
|
|
158
|
+
- Consumes: Task 1's `update_file`, `ensure_directory`, `erb(..., from:)`; `Generator#chmod`, `#sh`, `colors`; `@gem_name`, `@require_path`, `@gem_module` (set by `GemContext#detect_gem_context` in `New`).
|
|
159
|
+
- Produces: private `bootstrap_cli -> String | nil` (the gemspec path when it gained the `command_kit` dependency, so the caller knows a `bundle install` is due; `nil` otherwise). Constants `CliBootstrap::TEMPLATES`, `CLI_INFLECTION`, `TAP_SETUP`. Task 3 relies on `bootstrap_cli` being callable from `Create` with the same three ivars.
|
|
160
|
+
|
|
161
|
+
- [ ] **Step 1: Write the failing tests**
|
|
162
|
+
|
|
163
|
+
In `test/gempilot/cli/new_command_test.rb`, immediately before the `# --- Error handling ---` comment, add:
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
# --- CommandKit bootstrap ---
|
|
167
|
+
|
|
168
|
+
def test_new_command_bootstraps_cli_router
|
|
169
|
+
run_new_command("command", "deploy")
|
|
170
|
+
content = File.read("lib/my_gem/cli.rb")
|
|
171
|
+
|
|
172
|
+
assert_includes content, "class CLI"
|
|
173
|
+
assert_includes content, "include CommandKit::Commands"
|
|
174
|
+
assert_includes content, 'command_name "my_gem"'
|
|
175
|
+
assert_includes content, "version MyGem::VERSION"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def test_new_command_bootstraps_base_command
|
|
179
|
+
run_new_command("command", "deploy")
|
|
180
|
+
content = File.read("lib/my_gem/cli/command.rb")
|
|
181
|
+
|
|
182
|
+
assert_includes content, "class Command < CommandKit::Command"
|
|
183
|
+
assert_includes content, "include CommandKit::Interactive"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def test_new_command_creates_executable_starting_the_cli
|
|
187
|
+
run_new_command("command", "deploy")
|
|
188
|
+
content = File.read("exe/my_gem")
|
|
189
|
+
|
|
190
|
+
assert_predicate Pathname("exe/my_gem"), :executable?
|
|
191
|
+
assert_includes content, 'require "my_gem/cli"'
|
|
192
|
+
assert_includes content, "MyGem::CLI.start"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def test_new_command_makes_an_existing_executable_executable
|
|
196
|
+
FileUtils.mkdir_p("exe")
|
|
197
|
+
File.write("exe/my_gem", "#!/usr/bin/env ruby\nrequire \"my_gem/cli\"\nMyGem::CLI.start\n")
|
|
198
|
+
run_new_command("command", "deploy")
|
|
199
|
+
|
|
200
|
+
assert_predicate Pathname("exe/my_gem"), :executable?
|
|
201
|
+
assert_equal "#!/usr/bin/env ruby\nrequire \"my_gem/cli\"\nMyGem::CLI.start\n", File.read("exe/my_gem")
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def test_new_command_keeps_existing_cli_files
|
|
205
|
+
FileUtils.mkdir_p("lib/my_gem/cli")
|
|
206
|
+
File.write("lib/my_gem/cli.rb", "# custom router\n")
|
|
207
|
+
File.write("lib/my_gem/cli/command.rb", "# custom base\n")
|
|
208
|
+
run_new_command("command", "deploy")
|
|
209
|
+
|
|
210
|
+
assert_equal "# custom router\n", File.read("lib/my_gem/cli.rb")
|
|
211
|
+
assert_equal "# custom base\n", File.read("lib/my_gem/cli/command.rb")
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def test_new_command_adds_command_kit_dependency_to_gemspec
|
|
215
|
+
write_scaffolded_gem
|
|
216
|
+
run_new_command("command", "deploy")
|
|
217
|
+
gemspec = File.read("my_gem.gemspec")
|
|
218
|
+
|
|
219
|
+
assert_includes gemspec, 'spec.add_dependency "command_kit"'
|
|
220
|
+
assert_operator gemspec.index("command_kit"), :<, gemspec.index('"zeitwerk"')
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def test_new_command_does_not_duplicate_command_kit_dependency
|
|
224
|
+
write_scaffolded_gem
|
|
225
|
+
run_new_command("command", "deploy")
|
|
226
|
+
run_new_command("command", "status")
|
|
227
|
+
|
|
228
|
+
assert_equal 1, File.read("my_gem.gemspec").scan("command_kit").size
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def test_new_command_inflects_cli_in_the_loader
|
|
232
|
+
write_scaffolded_gem
|
|
233
|
+
run_new_command("command", "deploy")
|
|
234
|
+
|
|
235
|
+
expected = <<~RUBY
|
|
236
|
+
module MyGem
|
|
237
|
+
LOADER = Zeitwerk::Loader.for_gem.tap do |l|
|
|
238
|
+
l.inflector.inflect("cli" => "CLI")
|
|
239
|
+
l.setup
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
RUBY
|
|
243
|
+
|
|
244
|
+
assert_includes File.read("lib/my_gem.rb"), expected
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def test_new_command_inflects_cli_only_once
|
|
248
|
+
write_scaffolded_gem
|
|
249
|
+
run_new_command("command", "deploy")
|
|
250
|
+
run_new_command("command", "status")
|
|
251
|
+
|
|
252
|
+
assert_equal 1, File.read("lib/my_gem.rb").scan("inflect(").size
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def test_new_command_bundles_when_the_dependency_was_added
|
|
256
|
+
write_scaffolded_gem
|
|
257
|
+
File.write("Gemfile", "source \"https://rubygems.org\"\ngemspec\n")
|
|
258
|
+
sh_calls = run_new_command_recording_sh("command", "deploy")
|
|
259
|
+
|
|
260
|
+
assert_includes sh_calls, ["bundle", "install"]
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def test_new_command_does_not_bundle_when_the_dependency_was_present
|
|
264
|
+
write_scaffolded_gem
|
|
265
|
+
File.write("Gemfile", "source \"https://rubygems.org\"\ngemspec\n")
|
|
266
|
+
run_new_command("command", "deploy")
|
|
267
|
+
sh_calls = run_new_command_recording_sh("command", "status")
|
|
268
|
+
|
|
269
|
+
assert_empty sh_calls
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def test_new_command_in_hyphenated_gem_targets_the_nested_module
|
|
273
|
+
FileUtils.rm("my_gem.gemspec")
|
|
274
|
+
FileUtils.rm_rf("lib/my_gem")
|
|
275
|
+
File.write("my-gem.gemspec", 'Gem::Specification.new { |s| s.name = "my-gem" }')
|
|
276
|
+
FileUtils.mkdir_p("lib/my/gem")
|
|
277
|
+
run_new_command("command", "deploy")
|
|
278
|
+
|
|
279
|
+
assert_includes File.read("exe/my-gem"), 'require "my/gem/cli"'
|
|
280
|
+
assert_includes File.read("exe/my-gem"), "My::Gem::CLI.start"
|
|
281
|
+
assert_includes File.read("lib/my/gem/cli.rb"), "module My::Gem"
|
|
282
|
+
end
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
and, after the existing private `run_new_command` helper at the bottom of the class, add:
|
|
286
|
+
|
|
287
|
+
```ruby
|
|
288
|
+
def run_new_command_recording_sh(type, path)
|
|
289
|
+
sh_calls = []
|
|
290
|
+
command = Commands::New.new(stdout: StringIO.new)
|
|
291
|
+
command.define_singleton_method(:sh) { |*args| sh_calls << args }
|
|
292
|
+
command.main([type, path])
|
|
293
|
+
sh_calls
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def write_scaffolded_gem
|
|
297
|
+
File.write("lib/my_gem.rb", <<~RUBY)
|
|
298
|
+
require "zeitwerk"
|
|
299
|
+
|
|
300
|
+
module MyGem
|
|
301
|
+
LOADER = Zeitwerk::Loader.for_gem.tap(&:setup)
|
|
302
|
+
end
|
|
303
|
+
RUBY
|
|
304
|
+
File.write("my_gem.gemspec", <<~RUBY)
|
|
305
|
+
Gem::Specification.new do |spec|
|
|
306
|
+
spec.name = "my_gem"
|
|
307
|
+
spec.add_dependency "zeitwerk"
|
|
308
|
+
end
|
|
309
|
+
RUBY
|
|
310
|
+
end
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
The file's default fixture is deliberately minimal (a one-line gemspec with no `add_dependency` line and no `lib/my_gem.rb`); those tests prove the bootstrap degrades to a printed hint instead of crashing, while `write_scaffolded_gem` provides the realistic shape for the dependency and inflection tests.
|
|
314
|
+
|
|
315
|
+
- [ ] **Step 2: Run them to verify they fail**
|
|
316
|
+
|
|
317
|
+
Run: `bundle exec ruby -Itest -Ilib test/gempilot/cli/new_command_test.rb`
|
|
318
|
+
Expected: `31 runs, ... 12 failures/errors` — `Errno::ENOENT` for `lib/my_gem/cli.rb`, `exe/my_gem`, etc., the gemspec/loader assertions fail, and `sh_calls` is empty where `["bundle", "install"]` is expected. The 19 pre-existing tests still pass.
|
|
319
|
+
|
|
320
|
+
- [ ] **Step 3: Create the CLI templates**
|
|
321
|
+
|
|
322
|
+
Create `data/templates/cli/cli.rb.erb`:
|
|
323
|
+
|
|
324
|
+
```erb
|
|
325
|
+
require "command_kit/commands"
|
|
326
|
+
require "command_kit/commands/auto_load"
|
|
327
|
+
require "command_kit/options/version"
|
|
328
|
+
require "<%= @require_path %>"
|
|
329
|
+
|
|
330
|
+
module <%= @gem_module %>
|
|
331
|
+
##
|
|
332
|
+
# Top-level command router for the <%= @gem_name %> CLI.
|
|
333
|
+
class CLI
|
|
334
|
+
include CommandKit::Commands
|
|
335
|
+
include CommandKit::Commands::AutoLoad.new(
|
|
336
|
+
dir: "#{__dir__}/cli/commands",
|
|
337
|
+
namespace: "#{self}::Commands",
|
|
338
|
+
)
|
|
339
|
+
include CommandKit::Options::Version
|
|
340
|
+
|
|
341
|
+
command_name "<%= @gem_name %>"
|
|
342
|
+
version <%= @gem_module %>::VERSION
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Create `data/templates/cli/command.rb.erb`:
|
|
348
|
+
|
|
349
|
+
```erb
|
|
350
|
+
require "command_kit/command"
|
|
351
|
+
require "command_kit/colors"
|
|
352
|
+
require "command_kit/interactive"
|
|
353
|
+
|
|
354
|
+
module <%= @gem_module %>
|
|
355
|
+
class CLI
|
|
356
|
+
##
|
|
357
|
+
# Base command class for all <%= @gem_name %> subcommands.
|
|
358
|
+
class Command < CommandKit::Command
|
|
359
|
+
include CommandKit::Colors
|
|
360
|
+
include CommandKit::Interactive
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
(`CommandKit::BugReport` is left out on purpose: it needs a `bug_report_url`, which gempilot cannot know for someone else's gem.)
|
|
367
|
+
|
|
368
|
+
Replace the contents of `data/templates/new/command.rb.erb` with (only the doc block is new; the generated gem's `Style/Documentation` cop requires it):
|
|
369
|
+
|
|
370
|
+
```erb
|
|
371
|
+
require_relative "../command"
|
|
372
|
+
|
|
373
|
+
module <%= @gem_module %>
|
|
374
|
+
class CLI
|
|
375
|
+
module Commands
|
|
376
|
+
##
|
|
377
|
+
# The <%= @command_file_name %> command.
|
|
378
|
+
class <%= @command_name %> < Command
|
|
379
|
+
description "TODO: describe the <%= @command_file_name %> command"
|
|
380
|
+
|
|
381
|
+
def run
|
|
382
|
+
puts "TODO: implement <%= @command_file_name %>"
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
- [ ] **Step 4: Create the bootstrap mixin**
|
|
391
|
+
|
|
392
|
+
Create `lib/gempilot/cli/cli_bootstrap.rb`:
|
|
393
|
+
|
|
394
|
+
```ruby
|
|
395
|
+
module Gempilot
|
|
396
|
+
class CLI
|
|
397
|
+
##
|
|
398
|
+
# Scaffolds the CommandKit plumbing a generated command needs and the gem
|
|
399
|
+
# is missing: the +CLI+ router, the base +Command+ class, an executable
|
|
400
|
+
# that starts the router, the Zeitwerk inflection letting +cli.rb+ define
|
|
401
|
+
# +CLI+ rather than +Cli+, and the +command_kit+ runtime dependency.
|
|
402
|
+
#
|
|
403
|
+
# Every step is idempotent, so a gem that already has any of these pieces
|
|
404
|
+
# keeps them untouched. Expects the including command to provide the
|
|
405
|
+
# Generator methods and +@gem_name+, +@require_path+, and +@gem_module+.
|
|
406
|
+
module CliBootstrap
|
|
407
|
+
TEMPLATES = Gempilot::ROOT.join("data", "templates", "cli").to_s.freeze
|
|
408
|
+
CLI_INFLECTION = 'l.inflector.inflect("cli" => "CLI")'.freeze
|
|
409
|
+
TAP_SETUP = ".tap(&:setup)".freeze
|
|
410
|
+
|
|
411
|
+
private
|
|
412
|
+
|
|
413
|
+
# Returns the gemspec path when it gained the command_kit dependency
|
|
414
|
+
# (so the caller knows a bundle install is due), nil otherwise.
|
|
415
|
+
def bootstrap_cli
|
|
416
|
+
create_cli_router
|
|
417
|
+
create_base_command
|
|
418
|
+
create_executable
|
|
419
|
+
inflect_cli_constant
|
|
420
|
+
add_command_kit_dependency
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def create_cli_router
|
|
424
|
+
render_once "cli.rb.erb", File.join("lib", @require_path, "cli.rb")
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def create_base_command
|
|
428
|
+
render_once "command.rb.erb", File.join("lib", @require_path, "cli", "command.rb")
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def render_once(template, path)
|
|
432
|
+
return if File.exist?(path)
|
|
433
|
+
|
|
434
|
+
ensure_directory(File.dirname(path))
|
|
435
|
+
erb template, path, from: TEMPLATES
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def create_executable
|
|
439
|
+
path = File.join("exe", @gem_name)
|
|
440
|
+
render_once "exe.erb", path
|
|
441
|
+
chmod "+x", path unless File.executable?(path)
|
|
442
|
+
return if File.read(path).include?("CLI.start")
|
|
443
|
+
|
|
444
|
+
puts colors.yellow("#{path} does not start #{@gem_module}::CLI; add `#{@gem_module}::CLI.start` to it.")
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def inflect_cli_constant
|
|
448
|
+
path = File.join("lib", "#{@require_path}.rb")
|
|
449
|
+
source = File.exist?(path) ? File.read(path) : ""
|
|
450
|
+
return if source.include?(CLI_INFLECTION)
|
|
451
|
+
|
|
452
|
+
line = source.lines.find { it.include?(TAP_SETUP) }
|
|
453
|
+
return hint_inflection(path) unless line
|
|
454
|
+
|
|
455
|
+
update_file path, source.sub(TAP_SETUP, inflection_block(indent_of(line)))
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def inflection_block(indent)
|
|
459
|
+
[".tap do |l|", "#{indent} #{CLI_INFLECTION}", "#{indent} l.setup", "#{indent}end"].join("\n")
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def indent_of(line)
|
|
463
|
+
line[0, line.length - line.lstrip.length]
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def hint_inflection(path)
|
|
467
|
+
puts colors.yellow("Could not find #{TAP_SETUP} in #{path}; add #{CLI_INFLECTION} before the loader's setup.")
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def add_command_kit_dependency
|
|
471
|
+
path = "#{@gem_name}.gemspec"
|
|
472
|
+
lines = File.readlines(path)
|
|
473
|
+
return if lines.any? { it.include?("command_kit") }
|
|
474
|
+
|
|
475
|
+
index = lines.index { it.include?(".add_dependency") } || lines.rindex { it.strip == "end" }
|
|
476
|
+
index ? insert_dependency(path, lines, index) : hint_dependency(path)
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def insert_dependency(path, lines, index)
|
|
480
|
+
lines.insert(index, %( #{gemspec_receiver(lines)}.add_dependency "command_kit"\n))
|
|
481
|
+
update_file path, lines.join
|
|
482
|
+
path
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def gemspec_receiver(lines)
|
|
486
|
+
line = lines.find { it.include?(".add_dependency") }
|
|
487
|
+
line ? line.strip[0, line.strip.index(".add_dependency")] : "spec"
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def hint_dependency(path)
|
|
491
|
+
puts colors.yellow("Could not add command_kit to #{path}; add `spec.add_dependency \"command_kit\"` yourself.")
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
Why each piece looks the way it does:
|
|
499
|
+
- The inflection is mandatory, not cosmetic: Zeitwerk expects `lib/my_gem/cli.rb` to define `MyGem::Cli`; without `inflect("cli" => "CLI")` the gem's eager-load test and `rake zeitwerk:validate` fail with `Zeitwerk::NameError`. The edit turns the templates' `.tap(&:setup)` into the block form and indents relative to the `LOADER` line, so it is correct for both `for_gem` (2 spaces) and `for_gem_extension` (4 spaces). Only plain string operations are used (`include?`, `sub` with a String pattern, `lines`); no regex.
|
|
500
|
+
- The dependency goes before the first existing `.add_dependency` line (alphabetical for the generated gemspec, which has `zeitwerk`) using that line's receiver, or before the closing `end`; a gemspec with neither gets a hint instead of a broken edit.
|
|
501
|
+
- `insert_dependency` returns the path (truthy) rather than `true` because `Naming/PredicateMethod` rejects boolean-literal returns from methods not ending in `?`.
|
|
502
|
+
|
|
503
|
+
- [ ] **Step 5: Wire the bootstrap into `New`**
|
|
504
|
+
|
|
505
|
+
In `lib/gempilot/cli/commands/new.rb` add the include after `include GemContext`:
|
|
506
|
+
|
|
507
|
+
```ruby
|
|
508
|
+
include Generator
|
|
509
|
+
include GemContext
|
|
510
|
+
include CliBootstrap
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
Replace the `add_command` method with these three methods:
|
|
514
|
+
|
|
515
|
+
```ruby
|
|
516
|
+
def add_command(name)
|
|
517
|
+
name = name.split("::").last if name.include?("::")
|
|
518
|
+
@command_file_name = name.underscore
|
|
519
|
+
@command_name = name.camelize
|
|
520
|
+
|
|
521
|
+
print_adding_banner("command", @command_name)
|
|
522
|
+
dependency_added = bootstrap_cli
|
|
523
|
+
write_command_files
|
|
524
|
+
bundle_install if dependency_added
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def write_command_files
|
|
528
|
+
file_path = File.join("lib", @require_path, "cli", "commands", "#{@command_file_name}.rb")
|
|
529
|
+
ensure_directory(File.dirname(file_path))
|
|
530
|
+
erb "command.rb.erb", file_path
|
|
531
|
+
add_command_test_file(@command_name, @command_file_name)
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def bundle_install
|
|
535
|
+
return unless File.exist?("Gemfile")
|
|
536
|
+
|
|
537
|
+
sh "bundle", "install"
|
|
538
|
+
end
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
Replace `rspec_command_content` and `minitest_command_content` with versions that assert something the generated gem's RuboCop accepts (the old `assert command` trips `Minitest/UselessAssertion`), and add `command_line_name`:
|
|
542
|
+
|
|
543
|
+
```ruby
|
|
544
|
+
def rspec_command_content(command_name)
|
|
545
|
+
<<~RUBY
|
|
546
|
+
require "spec_helper"
|
|
547
|
+
|
|
548
|
+
RSpec.describe #{@gem_module}::CLI::Commands::#{command_name} do
|
|
549
|
+
it "is registered under its command name" do
|
|
550
|
+
expect(described_class.command_name).to eq("#{command_line_name}")
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
RUBY
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
def minitest_command_content(command_name)
|
|
557
|
+
<<~RUBY
|
|
558
|
+
require "test_helper"
|
|
559
|
+
require "#{@require_path}/cli"
|
|
560
|
+
|
|
561
|
+
module #{@gem_module}
|
|
562
|
+
class CLI
|
|
563
|
+
class #{command_name}Test < Minitest::Test
|
|
564
|
+
def test_command_name
|
|
565
|
+
assert_equal "#{command_line_name}", Commands::#{command_name}.command_name
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
end
|
|
570
|
+
RUBY
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
# The name CommandKit registers the command under: dashes, not
|
|
574
|
+
# underscores, matching how AutoLoad maps the file name.
|
|
575
|
+
def command_line_name
|
|
576
|
+
@command_file_name.tr("_", "-")
|
|
577
|
+
end
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
(`CommandKit::Command.command_name` defaults to the dasherized, demodulized class name, which is exactly what `Commands::AutoLoad` derives from the file name, so `DeployNow` in `deploy_now.rb` is `deploy-now` on both sides.)
|
|
581
|
+
|
|
582
|
+
- [ ] **Step 6: Run the tests, the new-command specs, and rubocop**
|
|
583
|
+
|
|
584
|
+
Run: `bundle exec ruby -Itest -Ilib test/gempilot/cli/new_command_test.rb && bundle exec rspec spec/gempilot/cli/commands/new_namespace_spec.rb spec/gempilot/cli/commands/new_interactive_spec.rb spec/zeitwerk_spec.rb --no-color && bundle exec rubocop lib/gempilot/cli/cli_bootstrap.rb lib/gempilot/cli/commands/new.rb test/gempilot/cli/new_command_test.rb`
|
|
585
|
+
Expected: `31 runs, 0 failures, 0 errors`; `13 examples, 0 failures`; `3 files inspected, no offenses detected`.
|
|
586
|
+
|
|
587
|
+
- [ ] **Step 7: Commit**
|
|
588
|
+
|
|
589
|
+
```bash
|
|
590
|
+
git add lib/gempilot/cli/cli_bootstrap.rb lib/gempilot/cli/commands/new.rb data/templates/cli data/templates/new/command.rb.erb test/gempilot/cli/new_command_test.rb
|
|
591
|
+
git commit -m "Bootstrap a CommandKit CLI when generating a command"
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
---
|
|
595
|
+
|
|
596
|
+
### Task 3: `gempilot create --exe` builds the same CLI
|
|
597
|
+
|
|
598
|
+
**Files:**
|
|
599
|
+
- Modify: `lib/gempilot/cli/commands/create.rb` (`include CliBootstrap`, `@gem_module`, option description)
|
|
600
|
+
- Modify: `lib/gempilot/cli/gem_builder.rb` (`render_executable`, doc block)
|
|
601
|
+
- Delete: `data/templates/gem/exe/gem_name.erb`
|
|
602
|
+
- Test: `test/gempilot/cli/create_command_test.rb`
|
|
603
|
+
|
|
604
|
+
**Interfaces:**
|
|
605
|
+
- Consumes: `bootstrap_cli` (Task 2), `GemBuilder#cd` (from `CommandKit::FileUtils`), `@module_name` computed in `Create#derive_naming`.
|
|
606
|
+
- Produces: `create --exe` output containing `lib/<gem>/cli.rb`, `lib/<gem>/cli/command.rb`, `exe/<gem>` (executable, starts `<Module>::CLI`), a gemspec with `spec.add_dependency "command_kit"`, and the loader inflection; `create` without `--exe` is unchanged.
|
|
607
|
+
|
|
608
|
+
- [ ] **Step 1: Write the failing tests**
|
|
609
|
+
|
|
610
|
+
In `test/gempilot/cli/create_command_test.rb`, immediately before `def test_inflects_module_name_correctly`, add:
|
|
611
|
+
|
|
612
|
+
```ruby
|
|
613
|
+
def test_exe_flag_bootstraps_a_command_kit_cli
|
|
614
|
+
run_create_command("test_gem", "--exe")
|
|
615
|
+
|
|
616
|
+
assert_includes File.read("test_gem/exe/test_gem"), "TestGem::CLI.start"
|
|
617
|
+
assert_includes File.read("test_gem/lib/test_gem/cli.rb"), "class CLI"
|
|
618
|
+
assert_includes File.read("test_gem/lib/test_gem/cli/command.rb"), "class Command < CommandKit::Command"
|
|
619
|
+
assert_includes File.read("test_gem/test_gem.gemspec"), 'spec.add_dependency "command_kit"'
|
|
620
|
+
assert_includes File.read("test_gem/lib/test_gem.rb"), 'l.inflector.inflect("cli" => "CLI")'
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
def test_no_exe_flag_leaves_the_cli_out
|
|
624
|
+
run_create_command("test_gem")
|
|
625
|
+
|
|
626
|
+
refute_path_exists "test_gem/lib/test_gem/cli.rb"
|
|
627
|
+
refute_includes File.read("test_gem/test_gem.gemspec"), "command_kit"
|
|
628
|
+
assert_includes File.read("test_gem/lib/test_gem.rb"), "for_gem.tap(&:setup)"
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
def test_hyphenated_gem_exe_flag_targets_the_extension_module
|
|
632
|
+
run_create_command("gempilot-encryption", "--exe")
|
|
633
|
+
exe = File.read("gempilot-encryption/exe/gempilot-encryption")
|
|
634
|
+
entry = File.read("gempilot-encryption/lib/gempilot/encryption.rb")
|
|
635
|
+
expected_loader = [
|
|
636
|
+
" LOADER = Zeitwerk::Loader.for_gem_extension(Gempilot).tap do |l|",
|
|
637
|
+
' l.inflector.inflect("cli" => "CLI")',
|
|
638
|
+
" l.setup",
|
|
639
|
+
" end",
|
|
640
|
+
].join("\n")
|
|
641
|
+
|
|
642
|
+
assert_includes exe, 'require "gempilot/encryption/cli"'
|
|
643
|
+
assert_includes exe, "Gempilot::Encryption::CLI.start"
|
|
644
|
+
assert_includes entry, expected_loader
|
|
645
|
+
end
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
and, immediately before the `private` keyword at the bottom of the class, the end-to-end test:
|
|
649
|
+
|
|
650
|
+
```ruby
|
|
651
|
+
def test_generated_cli_gem_runs_end_to_end
|
|
652
|
+
stdout = StringIO.new
|
|
653
|
+
Commands::Create.new(stdout: stdout).main(cli_gem_args)
|
|
654
|
+
patch_gemfile_gempilot_path("cli_gem/Gemfile")
|
|
655
|
+
|
|
656
|
+
Dir.chdir("cli_gem") do
|
|
657
|
+
Commands::New.new(stdout: stdout).main(["command", "deploy"])
|
|
658
|
+
|
|
659
|
+
Bundler.with_unbundled_env do
|
|
660
|
+
output = `bundle exec rake 2>&1`
|
|
661
|
+
|
|
662
|
+
assert_equal 0, $CHILD_STATUS.exitstatus, "Default rake task failed in CLI gem:\n#{output}"
|
|
663
|
+
assert_equal "cli_gem 0.0.1", `bundle exec exe/cli_gem --version 2>&1`.strip
|
|
664
|
+
assert_equal "TODO: implement deploy", `bundle exec exe/cli_gem deploy 2>&1`.strip
|
|
665
|
+
end
|
|
666
|
+
end
|
|
667
|
+
end
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
then, right after `private`, the argument helper:
|
|
671
|
+
|
|
672
|
+
```ruby
|
|
673
|
+
def cli_gem_args
|
|
674
|
+
["--author", "Test Author", "--email", "test@example.com", "--summary", "A test gem",
|
|
675
|
+
"--ruby-version", RUBY_VERSION, "--test", "minitest", "--exe", "--no-git", "cli_gem"]
|
|
676
|
+
end
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
The end-to-end test mirrors the existing `*_default_rake_task_passes` tests (real `bundle install`, Gemfile patched to the checkout) and additionally generates a command and runs the executable, so it proves the whole scaffold: Zeitwerk eager-loads the CLI files, the gem's RuboCop accepts them, `--version` prints `cli_gem 0.0.1`, and `deploy` dispatches through `AutoLoad`.
|
|
680
|
+
|
|
681
|
+
- [ ] **Step 2: Run the unit tests to verify they fail**
|
|
682
|
+
|
|
683
|
+
Run: `bundle exec ruby -Itest -Ilib test/gempilot/cli/create_command_test.rb -n "/exe_flag|cli_gem/"`
|
|
684
|
+
Expected: 4 runs; `test_exe_flag_bootstraps_a_command_kit_cli`, `test_hyphenated_gem_exe_flag_targets_the_extension_module` and the end-to-end test fail (`Errno::ENOENT` for `lib/test_gem/cli.rb`; the executable command is unknown), `test_no_exe_flag_leaves_the_cli_out` already passes.
|
|
685
|
+
|
|
686
|
+
- [ ] **Step 3: Wire the bootstrap into `Create`**
|
|
687
|
+
|
|
688
|
+
In `lib/gempilot/cli/commands/create.rb`:
|
|
689
|
+
|
|
690
|
+
```ruby
|
|
691
|
+
include Generator
|
|
692
|
+
include GemBuilder
|
|
693
|
+
include CliBootstrap
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
change the option description:
|
|
697
|
+
|
|
698
|
+
```ruby
|
|
699
|
+
option :exe, long: "--[no-]exe", desc: "Create a CommandKit CLI with an executable"
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
and in `derive_naming` set the ivar the CLI templates use, right after `@module_name`:
|
|
703
|
+
|
|
704
|
+
```ruby
|
|
705
|
+
@module_name = @require_path.camelize
|
|
706
|
+
@gem_module = @module_name
|
|
707
|
+
@module_parts = @module_name.split("::")
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
In `lib/gempilot/cli/gem_builder.rb` replace `render_executable` with:
|
|
711
|
+
|
|
712
|
+
```ruby
|
|
713
|
+
# The executable is one piece of the CommandKit CLI, so the whole
|
|
714
|
+
# scaffold (router, base command, exe, inflection, dependency) comes
|
|
715
|
+
# from CliBootstrap, run from inside the new gem.
|
|
716
|
+
def render_executable
|
|
717
|
+
return unless options[:exe]
|
|
718
|
+
|
|
719
|
+
cd(@gem_name) { bootstrap_cli }
|
|
720
|
+
end
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
and extend the module doc's ivar list:
|
|
724
|
+
|
|
725
|
+
```ruby
|
|
726
|
+
## +@gem_name+, +@require_path+, +@module_name+, +@gem_module+,
|
|
727
|
+
## +@hyphenated+, +@test_framework+, +@branch+.
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
Delete `data/templates/gem/exe/gem_name.erb` (`git rm data/templates/gem/exe/gem_name.erb`); `data/templates/cli/exe.erb` replaces it. `create_directories` keeps creating `exe/` up front so the printed scaffold listing is unchanged; `render_executable` runs before `run_bundle_install`, so the `command_kit` dependency the bootstrap adds is installed by create's own `bundle install`.
|
|
731
|
+
|
|
732
|
+
- [ ] **Step 4: Run the whole create test file, then the full gate**
|
|
733
|
+
|
|
734
|
+
Run: `bundle exec ruby -Itest -Ilib test/gempilot/cli/create_command_test.rb`
|
|
735
|
+
Expected: `57 runs, 0 failures, 0 errors` (this includes the three integration tests; allow a couple of minutes).
|
|
736
|
+
|
|
737
|
+
Run: `bundle exec rake default 2>&1 | tail -8`
|
|
738
|
+
Expected: minitest `128 runs, 0 failures`, RSpec `192 examples, 0 failures`, RuboCop `no offenses detected`.
|
|
739
|
+
|
|
740
|
+
- [ ] **Step 5: Commit**
|
|
741
|
+
|
|
742
|
+
```bash
|
|
743
|
+
git add lib/gempilot/cli/commands/create.rb lib/gempilot/cli/gem_builder.rb data/templates/gem/exe/gem_name.erb test/gempilot/cli/create_command_test.rb
|
|
744
|
+
git commit -m "Scaffold the CommandKit CLI for create --exe"
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
---
|
|
748
|
+
|
|
749
|
+
### Task 4: Documentation
|
|
750
|
+
|
|
751
|
+
**Files:**
|
|
752
|
+
- Modify: `README.md` (`gempilot create` options table, `gempilot new` section, Generated Gem Features)
|
|
753
|
+
- Modify: `CLAUDE.md` (Commands and Architecture lists)
|
|
754
|
+
|
|
755
|
+
**Interfaces:**
|
|
756
|
+
- Consumes: final behaviour of Tasks 2–3. No code.
|
|
757
|
+
|
|
758
|
+
- [ ] **Step 1: README**
|
|
759
|
+
|
|
760
|
+
In the `gempilot create` options table change the `--[no-]exe` row to:
|
|
761
|
+
|
|
762
|
+
```markdown
|
|
763
|
+
| `--[no-]exe` | Create a CommandKit CLI: `exe/<gem>`, `lib/<gem>/cli.rb`, base command, `command_kit` dependency | prompted |
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
Replace the paragraph under the `gempilot new` code block with:
|
|
767
|
+
|
|
768
|
+
```markdown
|
|
769
|
+
Creates the source file under `lib/` and a corresponding test file. For
|
|
770
|
+
commands, generates a CommandKit command class in `lib/<gem>/cli/commands/`
|
|
771
|
+
and, the first time, bootstraps the CLI around it: `lib/<gem>/cli.rb` (the
|
|
772
|
+
router), `lib/<gem>/cli/command.rb` (the base class), an executable
|
|
773
|
+
`exe/<gem>` that starts the router, the `command_kit` dependency in the
|
|
774
|
+
gemspec (followed by `bundle install`), and the Zeitwerk inflection
|
|
775
|
+
`"cli" => "CLI"` in `lib/<gem>.rb`. Pieces that already exist are left alone.
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
In "Generated Gem Features" add, after the Zeitwerk bullet:
|
|
779
|
+
|
|
780
|
+
```markdown
|
|
781
|
+
- **CommandKit CLI** when created with `--exe`: router, base command, and an
|
|
782
|
+
executable that works from the first commit
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
- [ ] **Step 2: CLAUDE.md**
|
|
786
|
+
|
|
787
|
+
Change the `gempilot new` command bullet to:
|
|
788
|
+
|
|
789
|
+
```markdown
|
|
790
|
+
- `gempilot new` — Generate a class, module, or command in an existing gem (templates in `data/templates/new/`); generating a command also bootstraps the CommandKit CLI when it is missing
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
and the `gempilot create` bullet to:
|
|
794
|
+
|
|
795
|
+
```markdown
|
|
796
|
+
- `gempilot create` — Scaffold a new gem (templates in `data/templates/gem/`); `--exe` adds a CommandKit CLI
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
Add to the Architecture list, after the `GemContext` bullet:
|
|
800
|
+
|
|
801
|
+
```markdown
|
|
802
|
+
- `CliBootstrap` module (`lib/gempilot/cli/cli_bootstrap.rb`) shared by `create --exe` and `new command`: idempotently scaffolds `lib/<gem>/cli.rb`, `lib/<gem>/cli/command.rb`, `exe/<gem>` (chmod +x), the `command_kit` gemspec dependency, and the `"cli" => "CLI"` loader inflection; its templates live in `data/templates/cli/` and are rendered through `Generator#erb(..., from:)`
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
- [ ] **Step 3: Commit**
|
|
806
|
+
|
|
807
|
+
```bash
|
|
808
|
+
git add README.md CLAUDE.md
|
|
809
|
+
git commit -m "Document the CommandKit CLI bootstrap"
|
|
810
|
+
```
|