gempilot 0.2.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.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/skills/using-command-kit/SKILL.md +119 -0
  3. data/.claude/skills/using-command-kit/cli-example.rb +84 -0
  4. data/.claude/skills/using-command-kit/generator-pattern.rb +62 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +281 -0
  7. data/.ruby-version +1 -0
  8. data/CLAUDE.md +45 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +140 -0
  11. data/Rakefile +44 -0
  12. data/data/templates/gem/Gemfile.erb +23 -0
  13. data/data/templates/gem/LICENSE.txt.erb +21 -0
  14. data/data/templates/gem/README.md.erb +25 -0
  15. data/data/templates/gem/Rakefile.erb +36 -0
  16. data/data/templates/gem/bin/console.erb +7 -0
  17. data/data/templates/gem/bin/setup.erb +5 -0
  18. data/data/templates/gem/dotfiles/github/workflows/ci.yml.erb +33 -0
  19. data/data/templates/gem/dotfiles/gitignore +11 -0
  20. data/data/templates/gem/dotfiles/rubocop.yml.erb +209 -0
  21. data/data/templates/gem/dotfiles/ruby-version.erb +1 -0
  22. data/data/templates/gem/exe/gem_name.erb +3 -0
  23. data/data/templates/gem/gemspec.erb +27 -0
  24. data/data/templates/gem/lib/gem_name/version.rb.erb +7 -0
  25. data/data/templates/gem/lib/gem_name.rb.erb +16 -0
  26. data/data/templates/gem/lib/gem_name_extension.rb.erb +20 -0
  27. data/data/templates/gem/rspec.erb +3 -0
  28. data/data/templates/gem/spec/gem_name_spec.rb.erb +5 -0
  29. data/data/templates/gem/spec/spec_helper.rb.erb +10 -0
  30. data/data/templates/gem/spec/zeitwerk_spec.rb.erb +5 -0
  31. data/data/templates/gem/test/gem_name_test.rb.erb +7 -0
  32. data/data/templates/gem/test/test_helper.rb.erb +7 -0
  33. data/data/templates/gem/test/zeitwerk_test.rb.erb +9 -0
  34. data/data/templates/new/.keep +0 -0
  35. data/data/templates/new/command.rb.erb +15 -0
  36. data/docs/command_kit_comparison.md +249 -0
  37. data/docs/command_kit_reference.md +517 -0
  38. data/docs/plans/2026-02-18-gempilot-add-command.md +718 -0
  39. data/docs/superpowers/plans/2026-04-01-rubocop-new-config.md +838 -0
  40. data/docs/superpowers/plans/2026-04-06-dogfood-inflectable.md +659 -0
  41. data/docs/superpowers/plans/2026-04-06-inflection-tests-and-erb-rename.md +166 -0
  42. data/docs/superpowers/plans/2026-04-06-integrate-version-tools.md +162 -0
  43. data/docs/superpowers/plans/2026-04-06-new-readme.md +185 -0
  44. data/docs/version-management-redesign.md +44 -0
  45. data/exe/gempilot +12 -0
  46. data/issues.rec +77 -0
  47. data/lib/core_ext/string/inflection_methods.rb +68 -0
  48. data/lib/core_ext/string/refinements/inflectable.rb +15 -0
  49. data/lib/gempilot/cli/command.rb +17 -0
  50. data/lib/gempilot/cli/commands/bump.rb +49 -0
  51. data/lib/gempilot/cli/commands/console.rb +38 -0
  52. data/lib/gempilot/cli/commands/create.rb +183 -0
  53. data/lib/gempilot/cli/commands/destroy.rb +136 -0
  54. data/lib/gempilot/cli/commands/new.rb +226 -0
  55. data/lib/gempilot/cli/commands/release.rb +40 -0
  56. data/lib/gempilot/cli/gem_builder.rb +105 -0
  57. data/lib/gempilot/cli/gem_context.rb +40 -0
  58. data/lib/gempilot/cli/generator.rb +90 -0
  59. data/lib/gempilot/cli.rb +19 -0
  60. data/lib/gempilot/github_release.rb +30 -0
  61. data/lib/gempilot/project/version.rb +39 -0
  62. data/lib/gempilot/project.rb +111 -0
  63. data/lib/gempilot/strict_shell.rb +18 -0
  64. data/lib/gempilot/version.rb +3 -0
  65. data/lib/gempilot/version_tag.rb +53 -0
  66. data/lib/gempilot/version_task.rb +108 -0
  67. data/lib/gempilot.rb +17 -0
  68. data/notes.md +31 -0
  69. metadata +165 -0
@@ -0,0 +1,718 @@
1
+ # `gempilot add` Implementation Plan
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** Add a `gempilot add <type> <path>` command that generates classes, modules, and commands inside existing gems with proper Zeitwerk-compliant file paths, module nesting, and test files.
6
+
7
+ **Architecture:** The `add` command detects the gem context (name, test framework) from the current directory, parses the user-provided path into namespace segments, generates the source file with properly nested `module`/`class` wrappers, and creates a matching test file. No ERB templates for class/module (nesting depth is variable) — content is built programmatically. The `command` type uses an ERB template since its structure is fixed.
8
+
9
+ **Tech Stack:** CommandKit (options, arguments, interactive, colors, inflector), Generator module (mkdir, print_action), ERB for command template, Minitest for tests.
10
+
11
+ ---
12
+
13
+ ## Key Design Decisions
14
+
15
+ **Why no ERB for class/module?** Nesting depth varies — `add class auth` is 1 level, `add class services/auth/token` is 3. Loops in ERB produce ugly templates. A Ruby method that builds the string is cleaner and testable.
16
+
17
+ **Why ERB for command?** Command boilerplate is fixed structure (always under `Gempilot::CLI::Commands`), so a template works well.
18
+
19
+ **Gem context detection:** Find `*.gemspec` in cwd. Extract gem name from filename. Detect test framework from `spec/` vs `test/` directory presence. No config file needed.
20
+
21
+ **Generator module reuse:** The `Add` command needs `mkdir` and `print_action` from Generator, but Generator requires `template_dir`. We'll set `template_dir` to `data/templates/add/` (for the command ERB) and add a `create_file(path, content)` helper to Generator for programmatic file creation.
22
+
23
+ ---
24
+
25
+ ### Task 0: Create swap space
26
+
27
+ This environment has limited memory. Create 4GB of swap before doing anything else.
28
+
29
+ **Step 1: Create and enable swap**
30
+
31
+ ```bash
32
+ sudo fallocate -l 4G /swapfile
33
+ sudo chmod 600 /swapfile
34
+ sudo mkswap /swapfile
35
+ sudo swapon /swapfile
36
+ ```
37
+
38
+ **Step 2: Verify**
39
+
40
+ ```bash
41
+ free -h
42
+ ```
43
+
44
+ Expected: Swap line shows ~4.0G total.
45
+
46
+ ---
47
+
48
+ ### Task 1: Add `create_file` helper to Generator
49
+
50
+ **Files:**
51
+ - Modify: `lib/gempilot/cli/generator.rb`
52
+ - Test: `test/gempilot/cli/generator_test.rb`
53
+
54
+ **Step 1: Write the failing test**
55
+
56
+ Add to `test/gempilot/cli/generator_test.rb`:
57
+
58
+ ```ruby
59
+ def test_create_file_writes_content
60
+ Dir.mktmpdir do |dir|
61
+ Dir.chdir(dir) do
62
+ path = "test_output.rb"
63
+ content = "# hello\n"
64
+
65
+ @generator.create_file(path, content)
66
+
67
+ assert_path_exists path
68
+ assert_equal content, File.read(path)
69
+ end
70
+ end
71
+ end
72
+ ```
73
+
74
+ Note: Check the existing generator test file first to understand how `@generator` is set up. Adapt the test to match the existing pattern (it may use a test command class with `include Generator` and `template_dir` set). The key assertion is: `create_file` writes the content and the file exists.
75
+
76
+ **Step 2: Run test to verify it fails**
77
+
78
+ Run: `cd /workspace/gempilot/misc-updates && ruby -Ilib -Itest test/gempilot/cli/generator_test.rb`
79
+ Expected: FAIL — `NoMethodError: undefined method 'create_file'`
80
+
81
+ **Step 3: Write minimal implementation**
82
+
83
+ Add to `lib/gempilot/cli/generator.rb`, after the `touch` method:
84
+
85
+ ```ruby
86
+ def create_file(path, content)
87
+ print_action "create", path
88
+ File.write(path, content)
89
+ end
90
+ ```
91
+
92
+ **Step 4: Run test to verify it passes**
93
+
94
+ Run: `cd /workspace/gempilot/misc-updates && ruby -Ilib -Itest test/gempilot/cli/generator_test.rb`
95
+ Expected: PASS
96
+
97
+ **Step 5: Commit**
98
+
99
+ ```bash
100
+ git add lib/gempilot/cli/generator.rb test/gempilot/cli/generator_test.rb
101
+ git commit -m "feat(generator): add create_file helper for programmatic file creation"
102
+ ```
103
+
104
+ ---
105
+
106
+ ### Task 2: Create the `add` command skeleton with gem context detection
107
+
108
+ **Files:**
109
+ - Create: `lib/gempilot/cli/commands/add.rb`
110
+ - Create: `test/gempilot/cli/add_command_test.rb`
111
+
112
+ **Step 1: Write the failing test**
113
+
114
+ Create `test/gempilot/cli/add_command_test.rb`:
115
+
116
+ ```ruby
117
+ require "test_helper"
118
+ require "gempilot/cli"
119
+ require "tmpdir"
120
+ require "stringio"
121
+
122
+ module Gempilot
123
+ class CLI
124
+ class AddCommandTest < Minitest::Test
125
+ def setup
126
+ @tmpdir = Dir.mktmpdir("add_command_test")
127
+ @original_dir = Dir.pwd
128
+
129
+ # Create a minimal gem directory to simulate being inside a gem
130
+ Dir.chdir(@tmpdir)
131
+ FileUtils.mkdir_p("lib/my_gem")
132
+ FileUtils.mkdir_p("test")
133
+ File.write("my_gem.gemspec", 'Gem::Specification.new { |s| s.name = "my_gem" }')
134
+ end
135
+
136
+ def teardown
137
+ Dir.chdir(@original_dir)
138
+ FileUtils.rm_rf(@tmpdir)
139
+ end
140
+
141
+ def test_add_class_creates_lib_file
142
+ run_add_command("class", "authentication")
143
+
144
+ assert_path_exists "lib/my_gem/authentication.rb"
145
+ end
146
+
147
+ def test_add_class_creates_correct_module_nesting
148
+ run_add_command("class", "authentication")
149
+
150
+ content = File.read("lib/my_gem/authentication.rb")
151
+
152
+ assert_includes content, "module MyGem"
153
+ assert_includes content, "class Authentication"
154
+ end
155
+
156
+ private
157
+
158
+ def run_add_command(type, path)
159
+ stdout = StringIO.new
160
+ command = Commands::Add.new(stdout: stdout)
161
+ command.main([type, path])
162
+ end
163
+ end
164
+ end
165
+ end
166
+ ```
167
+
168
+ **Step 2: Run test to verify it fails**
169
+
170
+ Run: `cd /workspace/gempilot/misc-updates && ruby -Ilib -Itest test/gempilot/cli/add_command_test.rb`
171
+ Expected: FAIL — `NameError: uninitialized constant Commands::Add`
172
+
173
+ **Step 3: Write minimal implementation**
174
+
175
+ Create `data/templates/add/` directory (empty for now — needed by Generator):
176
+ ```bash
177
+ mkdir -p /workspace/gempilot/misc-updates/data/templates/add
178
+ touch /workspace/gempilot/misc-updates/data/templates/add/.keep
179
+ ```
180
+
181
+ Create `lib/gempilot/cli/commands/add.rb`:
182
+
183
+ ```ruby
184
+ require_relative "../command"
185
+ require_relative "../generator"
186
+ require "command_kit/inflector"
187
+
188
+ module Gempilot
189
+ class CLI
190
+ module Commands
191
+ class Add < Command
192
+ include Generator
193
+
194
+ template_dir File.join(Gempilot::ROOT, "data", "templates", "add")
195
+
196
+ usage "[options] TYPE PATH"
197
+ description "Add a class, module, or command to an existing gem"
198
+
199
+ examples [
200
+ "class authentication",
201
+ "class services/authentication",
202
+ "module middleware",
203
+ "command deploy"
204
+ ]
205
+
206
+ argument :type, required: true,
207
+ desc: "Type to generate (class, module, command)"
208
+
209
+ argument :path, required: true,
210
+ desc: "Path relative to gem namespace (e.g., services/authentication)"
211
+
212
+ def run(type = nil, path = nil)
213
+ type = type || begin
214
+ puts colors.bright_black("What kind of component do you want to add?")
215
+ ask_multiple_choice(colors.green("Type"), %w[class module command])
216
+ end
217
+
218
+ path = path || begin
219
+ puts
220
+ puts colors.bright_black("Path relative to the gem namespace (e.g., services/authentication).")
221
+ ask(colors.green("Path"), required: true)
222
+ end
223
+
224
+ detect_gem_context
225
+
226
+ case type
227
+ when "class" then add_class(path)
228
+ when "module" then add_module(path)
229
+ when "command" then add_command(path)
230
+ else
231
+ puts colors.red("Unknown type '#{type}'. Use class, module, or command.")
232
+ exit 1
233
+ end
234
+ end
235
+
236
+ private
237
+
238
+ def detect_gem_context
239
+ gemspec = Dir.glob("*.gemspec").first
240
+
241
+ unless gemspec
242
+ puts colors.red("No gemspec found in current directory. Run this from your gem's root.")
243
+ exit 1
244
+ end
245
+
246
+ @gem_name = File.basename(gemspec, ".gemspec")
247
+ @gem_module = CommandKit::Inflector.camelize(@gem_name)
248
+ @test_framework = File.directory?("spec") ? :rspec : :minitest
249
+ end
250
+
251
+ def build_nested_source(namespaces, type_keyword, name)
252
+ lines = []
253
+ namespaces.each_with_index do |ns, i|
254
+ lines << "#{" " * i}module #{ns}"
255
+ end
256
+
257
+ depth = namespaces.length
258
+ lines << "#{" " * depth}#{type_keyword} #{name}"
259
+ lines << "#{" " * depth}end"
260
+
261
+ namespaces.length.times do |i|
262
+ lines << "#{" " * (namespaces.length - 1 - i)}end"
263
+ end
264
+
265
+ lines.join("\n") + "\n"
266
+ end
267
+
268
+ def parse_path(path)
269
+ segments = path.tr("-", "_").split("/")
270
+ modules = segments[0...-1].map { |s| CommandKit::Inflector.camelize(s) }
271
+ name = CommandKit::Inflector.camelize(segments.last)
272
+ [segments, modules, name]
273
+ end
274
+
275
+ def add_class(path)
276
+ segments, intermediate_modules, class_name = parse_path(path)
277
+ namespaces = [@gem_module] + intermediate_modules
278
+ file_path = File.join("lib", @gem_name, *segments) + ".rb"
279
+
280
+ puts
281
+ puts colors.bright_white("Adding class ") + colors.bold(colors.cyan("#{namespaces.join('::')}::#{class_name}")) + colors.bright_white("...")
282
+ puts
283
+
284
+ # Create directories
285
+ dir = File.dirname(file_path)
286
+ mkdir(dir) unless File.directory?(dir)
287
+
288
+ # Create lib file
289
+ source = "# frozen_string_literal: true\n\n" + build_nested_source(namespaces, "class", class_name)
290
+ create_file(file_path, source)
291
+
292
+ # Create test file
293
+ add_test_file(namespaces, class_name, segments)
294
+ end
295
+
296
+ def add_module(path)
297
+ segments, intermediate_modules, mod_name = parse_path(path)
298
+ namespaces = [@gem_module] + intermediate_modules
299
+ file_path = File.join("lib", @gem_name, *segments) + ".rb"
300
+
301
+ puts
302
+ puts colors.bright_white("Adding module ") + colors.bold(colors.cyan("#{namespaces.join('::')}::#{mod_name}")) + colors.bright_white("...")
303
+ puts
304
+
305
+ dir = File.dirname(file_path)
306
+ mkdir(dir) unless File.directory?(dir)
307
+
308
+ source = "# frozen_string_literal: true\n\n" + build_nested_source(namespaces, "module", mod_name)
309
+ create_file(file_path, source)
310
+ end
311
+
312
+ def add_command(path)
313
+ segments, _, command_name = parse_path(path)
314
+ file_path = File.join("lib", @gem_name, "cli", "commands", segments.last + ".rb")
315
+
316
+ puts
317
+ puts colors.bright_white("Adding command ") + colors.bold(colors.cyan(command_name)) + colors.bright_white("...")
318
+ puts
319
+
320
+ dir = File.dirname(file_path)
321
+ mkdir(dir) unless File.directory?(dir)
322
+
323
+ @command_name = command_name
324
+ @command_file_name = segments.last
325
+ erb "command.rb.erb", file_path
326
+ end
327
+
328
+ def add_test_file(namespaces, class_name, segments)
329
+ if @test_framework == :rspec
330
+ test_path = File.join("spec", @gem_name, *segments) + "_spec.rb"
331
+ test_dir = File.dirname(test_path)
332
+ mkdir(test_dir) unless File.directory?(test_dir)
333
+
334
+ content = <<~RUBY
335
+ # frozen_string_literal: true
336
+
337
+ require "spec_helper"
338
+
339
+ RSpec.describe #{namespaces.join('::')}::#{class_name} do
340
+ pending "add some examples"
341
+ end
342
+ RUBY
343
+ create_file(test_path, content)
344
+ else
345
+ test_path = File.join("test", @gem_name, *segments) + "_test.rb"
346
+ test_dir = File.dirname(test_path)
347
+ mkdir(test_dir) unless File.directory?(test_dir)
348
+
349
+ content = <<~RUBY
350
+ # frozen_string_literal: true
351
+
352
+ require "test_helper"
353
+
354
+ module #{namespaces.first}
355
+ class #{(namespaces[1..] + [class_name]).join('::')}Test < Minitest::Test
356
+ def test_placeholder
357
+ assert true
358
+ end
359
+ end
360
+ end
361
+ RUBY
362
+ create_file(test_path, content)
363
+ end
364
+ end
365
+ end
366
+ end
367
+ end
368
+ end
369
+ ```
370
+
371
+ **Step 4: Run test to verify it passes**
372
+
373
+ Run: `cd /workspace/gempilot/misc-updates && ruby -Ilib -Itest test/gempilot/cli/add_command_test.rb`
374
+ Expected: PASS
375
+
376
+ **Step 5: Commit**
377
+
378
+ ```bash
379
+ git add lib/gempilot/cli/commands/add.rb test/gempilot/cli/add_command_test.rb data/templates/add/.keep
380
+ git commit -m "feat: add 'gempilot add' command with class generation"
381
+ ```
382
+
383
+ ---
384
+
385
+ ### Task 3: Add tests for nested paths and module nesting
386
+
387
+ **Files:**
388
+ - Modify: `test/gempilot/cli/add_command_test.rb`
389
+
390
+ **Step 1: Write the failing tests**
391
+
392
+ Add to the test class:
393
+
394
+ ```ruby
395
+ def test_add_class_with_nested_path_creates_directories
396
+ run_add_command("class", "services/authentication")
397
+
398
+ assert File.directory?("lib/my_gem/services")
399
+ assert_path_exists "lib/my_gem/services/authentication.rb"
400
+ end
401
+
402
+ def test_add_class_with_nested_path_has_correct_nesting
403
+ run_add_command("class", "services/authentication")
404
+
405
+ content = File.read("lib/my_gem/services/authentication.rb")
406
+
407
+ assert_includes content, "module MyGem"
408
+ assert_includes content, "module Services"
409
+ assert_includes content, "class Authentication"
410
+ end
411
+
412
+ def test_add_class_with_deeply_nested_path
413
+ run_add_command("class", "services/auth/token_validator")
414
+
415
+ content = File.read("lib/my_gem/services/auth/token_validator.rb")
416
+
417
+ assert_includes content, "module MyGem"
418
+ assert_includes content, "module Services"
419
+ assert_includes content, "module Auth"
420
+ assert_includes content, "class TokenValidator"
421
+ end
422
+
423
+ def test_add_class_creates_frozen_string_literal
424
+ run_add_command("class", "authentication")
425
+
426
+ content = File.read("lib/my_gem/authentication.rb")
427
+
428
+ assert content.start_with?("# frozen_string_literal: true")
429
+ end
430
+ ```
431
+
432
+ **Step 2: Run tests to verify they pass**
433
+
434
+ Run: `cd /workspace/gempilot/misc-updates && ruby -Ilib -Itest test/gempilot/cli/add_command_test.rb`
435
+ Expected: PASS (implementation from Task 2 should handle these)
436
+
437
+ If any fail, fix the implementation and re-run.
438
+
439
+ **Step 3: Commit**
440
+
441
+ ```bash
442
+ git add test/gempilot/cli/add_command_test.rb
443
+ git commit -m "test: add nested path tests for gempilot add class"
444
+ ```
445
+
446
+ ---
447
+
448
+ ### Task 4: Add tests for test file generation
449
+
450
+ **Files:**
451
+ - Modify: `test/gempilot/cli/add_command_test.rb`
452
+
453
+ **Step 1: Write the failing tests**
454
+
455
+ ```ruby
456
+ def test_add_class_creates_minitest_file
457
+ run_add_command("class", "authentication")
458
+
459
+ assert_path_exists "test/my_gem/authentication_test.rb"
460
+
461
+ content = File.read("test/my_gem/authentication_test.rb")
462
+
463
+ assert_includes content, "require \"test_helper\""
464
+ assert_includes content, "module MyGem"
465
+ assert_includes content, "Minitest::Test"
466
+ end
467
+
468
+ def test_add_class_creates_rspec_file_when_spec_dir_exists
469
+ # Switch to rspec by creating spec/ and removing test/
470
+ FileUtils.rm_rf("test")
471
+ FileUtils.mkdir_p("spec")
472
+
473
+ run_add_command("class", "authentication")
474
+
475
+ assert_path_exists "spec/my_gem/authentication_spec.rb"
476
+
477
+ content = File.read("spec/my_gem/authentication_spec.rb")
478
+
479
+ assert_includes content, "require \"spec_helper\""
480
+ assert_includes content, "RSpec.describe MyGem::Authentication"
481
+ end
482
+
483
+ def test_add_class_creates_nested_test_file
484
+ run_add_command("class", "services/authentication")
485
+
486
+ assert_path_exists "test/my_gem/services/authentication_test.rb"
487
+ end
488
+ ```
489
+
490
+ **Step 2: Run tests**
491
+
492
+ Run: `cd /workspace/gempilot/misc-updates && ruby -Ilib -Itest test/gempilot/cli/add_command_test.rb`
493
+ Expected: PASS
494
+
495
+ **Step 3: Commit**
496
+
497
+ ```bash
498
+ git add test/gempilot/cli/add_command_test.rb
499
+ git commit -m "test: add test file generation tests for gempilot add"
500
+ ```
501
+
502
+ ---
503
+
504
+ ### Task 5: Add module generation and tests
505
+
506
+ **Files:**
507
+ - Modify: `test/gempilot/cli/add_command_test.rb`
508
+
509
+ **Step 1: Write the failing tests**
510
+
511
+ ```ruby
512
+ def test_add_module_creates_lib_file
513
+ run_add_command("module", "middleware")
514
+
515
+ assert_path_exists "lib/my_gem/middleware.rb"
516
+
517
+ content = File.read("lib/my_gem/middleware.rb")
518
+
519
+ assert_includes content, "module MyGem"
520
+ assert_includes content, "module Middleware"
521
+ refute_includes content, "class"
522
+ end
523
+
524
+ def test_add_module_does_not_create_test_file
525
+ run_add_command("module", "middleware")
526
+
527
+ refute_path_exists "test/my_gem/middleware_test.rb"
528
+ end
529
+
530
+ def test_add_module_with_nested_path
531
+ run_add_command("module", "services/concerns")
532
+
533
+ content = File.read("lib/my_gem/services/concerns.rb")
534
+
535
+ assert_includes content, "module MyGem"
536
+ assert_includes content, "module Services"
537
+ assert_includes content, "module Concerns"
538
+ end
539
+ ```
540
+
541
+ **Step 2: Run tests**
542
+
543
+ Run: `cd /workspace/gempilot/misc-updates && ruby -Ilib -Itest test/gempilot/cli/add_command_test.rb`
544
+ Expected: PASS (implementation from Task 2 covers modules)
545
+
546
+ **Step 3: Commit**
547
+
548
+ ```bash
549
+ git add test/gempilot/cli/add_command_test.rb
550
+ git commit -m "test: add module generation tests for gempilot add"
551
+ ```
552
+
553
+ ---
554
+
555
+ ### Task 6: Add command generation with ERB template
556
+
557
+ **Files:**
558
+ - Create: `data/templates/add/command.rb.erb`
559
+ - Modify: `test/gempilot/cli/add_command_test.rb`
560
+
561
+ **Step 1: Write the failing test**
562
+
563
+ ```ruby
564
+ def test_add_command_creates_command_file
565
+ # Need cli/commands directory to exist (simulates a CLI gem)
566
+ FileUtils.mkdir_p("lib/my_gem/cli/commands")
567
+
568
+ run_add_command("command", "deploy")
569
+
570
+ assert_path_exists "lib/my_gem/cli/commands/deploy.rb"
571
+
572
+ content = File.read("lib/my_gem/cli/commands/deploy.rb")
573
+
574
+ assert_includes content, "class Deploy < Command"
575
+ assert_includes content, "module MyGem"
576
+ assert_includes content, "module Commands"
577
+ assert_includes content, 'description'
578
+ end
579
+ ```
580
+
581
+ **Step 2: Run test to verify it fails**
582
+
583
+ Run: `cd /workspace/gempilot/misc-updates && ruby -Ilib -Itest test/gempilot/cli/add_command_test.rb`
584
+ Expected: FAIL — template file missing, `Errno::ENOENT`
585
+
586
+ **Step 3: Create the ERB template**
587
+
588
+ Create `data/templates/add/command.rb.erb`:
589
+
590
+ ```erb
591
+ # frozen_string_literal: true
592
+
593
+ require_relative "../command"
594
+
595
+ module <%= @gem_module %>
596
+ class CLI
597
+ module Commands
598
+ class <%= @command_name %> < Command
599
+ usage "[options]"
600
+ description "TODO: describe the <%= @command_file_name %> command"
601
+
602
+ def run
603
+ end
604
+ end
605
+ end
606
+ end
607
+ end
608
+ ```
609
+
610
+ **Step 4: Run test to verify it passes**
611
+
612
+ Run: `cd /workspace/gempilot/misc-updates && ruby -Ilib -Itest test/gempilot/cli/add_command_test.rb`
613
+ Expected: PASS
614
+
615
+ **Step 5: Commit**
616
+
617
+ ```bash
618
+ git add data/templates/add/command.rb.erb test/gempilot/cli/add_command_test.rb
619
+ git commit -m "feat: add command generation with ERB template"
620
+ ```
621
+
622
+ ---
623
+
624
+ ### Task 7: Add error handling test
625
+
626
+ **Files:**
627
+ - Modify: `test/gempilot/cli/add_command_test.rb`
628
+
629
+ **Step 1: Write the failing test**
630
+
631
+ ```ruby
632
+ def test_add_fails_without_gemspec
633
+ # Remove gemspec
634
+ FileUtils.rm("my_gem.gemspec")
635
+
636
+ stdout = StringIO.new
637
+ command = Commands::Add.new(stdout: stdout)
638
+
639
+ assert_raises(SystemExit) do
640
+ command.main(["class", "foo"])
641
+ end
642
+ end
643
+
644
+ def test_add_fails_with_unknown_type
645
+ stdout = StringIO.new
646
+ command = Commands::Add.new(stdout: stdout)
647
+
648
+ assert_raises(SystemExit) do
649
+ command.main(["widget", "foo"])
650
+ end
651
+ end
652
+ ```
653
+
654
+ **Step 2: Run tests**
655
+
656
+ Run: `cd /workspace/gempilot/misc-updates && ruby -Ilib -Itest test/gempilot/cli/add_command_test.rb`
657
+ Expected: PASS
658
+
659
+ **Step 3: Commit**
660
+
661
+ ```bash
662
+ git add test/gempilot/cli/add_command_test.rb
663
+ git commit -m "test: add error handling tests for gempilot add"
664
+ ```
665
+
666
+ ---
667
+
668
+ ### Task 8: Run full test suite and verify
669
+
670
+ **Step 1: Run all tests**
671
+
672
+ Run: `cd /workspace/gempilot/misc-updates && ruby -Ilib -Itest -e 'Dir.glob("test/**/*_test.rb").each { |f| require_relative f }'`
673
+ Expected: All tests pass (30 existing + new add command tests)
674
+
675
+ **Step 2: Manual smoke test**
676
+
677
+ ```bash
678
+ cd /tmp
679
+ ruby -I/workspace/gempilot/misc-updates/lib -e 'require "gempilot/cli"; Gempilot::CLI.start' -- new smoke_test --test minitest --no-exe --no-git --author Test --email t@t.com --summary Test --ruby-version 3.4.8
680
+ cd smoke_test
681
+ ruby -I/workspace/gempilot/misc-updates/lib -e 'require "gempilot/cli"; Gempilot::CLI.start' -- add class services/authentication
682
+ ruby -I/workspace/gempilot/misc-updates/lib -e 'require "gempilot/cli"; Gempilot::CLI.start' -- add module middleware
683
+ cat lib/smoke_test/services/authentication.rb
684
+ cat lib/smoke_test/middleware.rb
685
+ cat test/smoke_test/services/authentication_test.rb
686
+ ```
687
+
688
+ Expected output for `lib/smoke_test/services/authentication.rb`:
689
+ ```ruby
690
+ # frozen_string_literal: true
691
+
692
+ module SmokeTest
693
+ module Services
694
+ class Authentication
695
+ end
696
+ end
697
+ end
698
+ ```
699
+
700
+ **Step 3: Commit (if any fixes needed)**
701
+
702
+ ```bash
703
+ git add -A
704
+ git commit -m "fix: address issues found in smoke test"
705
+ ```
706
+
707
+ ---
708
+
709
+ ## Summary of files
710
+
711
+ | File | Action |
712
+ |------|--------|
713
+ | `lib/gempilot/cli/generator.rb` | Add `create_file` method |
714
+ | `lib/gempilot/cli/commands/add.rb` | Create — the Add command |
715
+ | `data/templates/add/command.rb.erb` | Create — ERB template for command type |
716
+ | `data/templates/add/.keep` | Create — keeps directory in git |
717
+ | `test/gempilot/cli/add_command_test.rb` | Create — comprehensive tests |
718
+ | `test/gempilot/cli/generator_test.rb` | Modify — test for `create_file` |