gempilot 0.2.1 → 0.2.2
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/CLAUDE.md +3 -2
- data/docs/superpowers/plans/2026-06-09-address-review-critiques.md +679 -0
- data/lib/gempilot/cli/commands/destroy.rb +8 -23
- data/lib/gempilot/cli/commands/new.rb +18 -36
- data/lib/gempilot/cli/gem_context.rb +9 -13
- data/lib/gempilot/gem_constant.rb +61 -0
- data/lib/gempilot/version.rb +1 -1
- data/lib/gempilot/version_tag.rb +6 -2
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 48657265841f0000324b7cfca38d98ed1a45a59e7906810660a8e9e047042286
|
|
4
|
+
data.tar.gz: 9a084c85bfeec3ea094da9e7b7b245a0e30a050631db84acf8d434e88470b948
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 677fa36cce5bf93834debebf366296289b5fe69baf1ecb325d2169aa03140e928373a36052d4e4accfff37aebdfdb5562bbdda09244c5e6e15b1f51b982397c9
|
|
7
|
+
data.tar.gz: 8afb56703f50f6f42c56b7049167c498656576c8c4e5061cdd11883b9d32efe0ea7906203391dc809d4197c222f654010439fd38ead128ce9783446dafa50e54
|
data/CLAUDE.md
CHANGED
|
@@ -20,6 +20,7 @@ A CLI tool for creating and managing Ruby gems, built on CommandKit.
|
|
|
20
20
|
- Uses CommandKit::Inflector for name inflection
|
|
21
21
|
- Generator module (`lib/gempilot/cli/generator.rb`) provides template rendering via ERB
|
|
22
22
|
- GemContext module (`lib/gempilot/cli/gem_context.rb`) shared by new, destroy, release, console
|
|
23
|
+
- `GemConstant` value object (`lib/gempilot/gem_constant.rb`) owns constant→namespace/path resolution for `new`/`destroy`; constants are rooted at the gem module by construction
|
|
23
24
|
- CommandKit::Commands::AutoLoad maps filenames in `commands/` to command names
|
|
24
25
|
|
|
25
26
|
### Testing
|
|
@@ -34,7 +35,7 @@ A CLI tool for creating and managing Ruby gems, built on CommandKit.
|
|
|
34
35
|
- RuboCop with framework-specific plugins
|
|
35
36
|
- GitHub Actions CI workflow (`.github/workflows/ci.yml`)
|
|
36
37
|
- `git ls-files`-based gemspec with glob fallback for non-git repos
|
|
37
|
-
- Version
|
|
38
|
+
- Version lifecycle rake tasks installed via `Gempilot::VersionTask.new` (a `Rake::TaskLib`): `version:current/bump/commit/tag/untag/reset/revert`, composite `version:release`/`version:unrelease`, and `version:github:release/unrelease/list`
|
|
38
39
|
|
|
39
40
|
### Notes
|
|
40
41
|
- AutoLoad uses block form in `cli.rb` with explicit `summary:` per command (lazy loading means descriptions aren't available at help time without this)
|
|
@@ -42,4 +43,4 @@ A CLI tool for creating and managing Ruby gems, built on CommandKit.
|
|
|
42
43
|
|
|
43
44
|
## ISSUES
|
|
44
45
|
|
|
45
|
-
1. RESOLVED — `exe/gempilot` had unconditional `ENV["BUNDLE_GEMFILE"]`
|
|
46
|
+
1. RESOLVED — `exe/gempilot` previously had an unconditional `ENV["BUNDLE_GEMFILE"]` + `require "bundler/setup"`, breaking `gem install` usage. It now guards both behind `if File.exist?(gemfile)` (the Ronin pattern), so installed gems skip Bundler while in-repo runs still use it.
|
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
# Address Code-Review Critiques 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:** Retire a class of namespace/path bugs and the `new`/`destroy` duplication by introducing a `GemConstant` value object, plus small polish to the version tasks, framework detection, and stale docs.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Extract the duplicated "constant within a gem" logic (qualify, namespaces, lib/test paths) out of `new.rb`, `destroy.rb`, and `gem_context.rb` into one immutable `Gempilot::GemConstant` (a `Data` subclass), mirroring the existing `Project::Version` value object. Commands build a `GemConstant` via a shared `GemContext#gem_constant` factory and read paths off it. Smaller, independent fixes round out the other critiques.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Ruby 4.0, CommandKit, Zeitwerk, RSpec 3.13 + Minitest, RuboCop (+ rubocop-rspec).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Environment note
|
|
14
|
+
|
|
15
|
+
`bundle exec` is broken on this box (vendored gems are macOS-built; see memory `gempilot-test-running-env`). Run tools directly against system gems:
|
|
16
|
+
- RSpec: `rspec <path>` (full suite: `rspec`)
|
|
17
|
+
- Minitest: `ruby -Itest -Ilib -e 'Dir["test/**/*_test.rb"].each { |f| require File.expand_path(f) }'`
|
|
18
|
+
- RuboCop: `rubocop <paths>`
|
|
19
|
+
|
|
20
|
+
In a normal environment these are `bundle exec rspec` / `bundle exec rake test` / `bundle exec rake rubocop`.
|
|
21
|
+
|
|
22
|
+
## File structure
|
|
23
|
+
|
|
24
|
+
| File | Responsibility | Change |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| `lib/gempilot/gem_constant.rb` | Value object: a class/module constant within a gem (qualify + paths) | **Create** |
|
|
27
|
+
| `spec/gempilot/gem_constant_spec.rb` | Unit spec for `GemConstant` | **Create** |
|
|
28
|
+
| `lib/gempilot/cli/gem_context.rb` | Shared command context; `gem_constant` factory + framework detection | Modify |
|
|
29
|
+
| `lib/gempilot/cli/commands/new.rb` | `new` command, now delegating constant logic to `GemConstant` | Modify |
|
|
30
|
+
| `lib/gempilot/cli/commands/destroy.rb` | `destroy` command, same delegation | Modify |
|
|
31
|
+
| `lib/gempilot/version_tag.rb` | Version git ops: consistent error + named constant | Modify |
|
|
32
|
+
| `spec/gempilot/version_tag_spec.rb` | Update the non-bump assertion expectation | Modify |
|
|
33
|
+
| `spec/gempilot/cli/commands/new_namespace_spec.rb` | Document unified non-interactive rooting | Modify |
|
|
34
|
+
| `CLAUDE.md` | Fix stale architecture/feature notes | Modify |
|
|
35
|
+
|
|
36
|
+
**Design decision (documented in `GemConstant`):** every class/module constant is rooted at the gem's module *by construction* — bare input is prefixed, input already starting with the gem's first segment is left as-is. This makes interactive and non-interactive behavior consistent and removes the half-enforced `validate_gem_root!` (no test depends on its rejection). Rooting matches on the first segment only, preserving extension-gem support (a `My::Gem` gem accepts any `My::…`).
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Part A — Version task polish
|
|
41
|
+
|
|
42
|
+
### Task A1: Make the non-bump guard `raise` instead of `abort`
|
|
43
|
+
|
|
44
|
+
**Files:**
|
|
45
|
+
- Modify: `lib/gempilot/version_tag.rb:50`
|
|
46
|
+
- Test: `spec/gempilot/version_tag_spec.rb:109-118`
|
|
47
|
+
|
|
48
|
+
- [ ] **Step 1: Update the spec to expect a `RuntimeError`** (consistent with `#create`'s dirty-staging error)
|
|
49
|
+
|
|
50
|
+
In `spec/gempilot/version_tag_spec.rb`, replace the block at lines 109-118 with:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
describe "#tag when last commit is not a version bump" do
|
|
54
|
+
before do
|
|
55
|
+
File.write("README.md", "init")
|
|
56
|
+
system("git add README.md && git commit -m 'Not a version bump' --quiet")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "raises an error" do
|
|
60
|
+
expect { version_tag.tag }.to raise_error(RuntimeError, /does not appear to be a version bump/)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- [ ] **Step 2: Run the spec, verify it fails**
|
|
66
|
+
|
|
67
|
+
Run: `rspec spec/gempilot/version_tag_spec.rb -e "not a version bump"`
|
|
68
|
+
Expected: FAIL — `expected RuntimeError, got #<SystemExit: exit>` (code still calls `abort`).
|
|
69
|
+
|
|
70
|
+
- [ ] **Step 3: Change `abort` to `raise`**
|
|
71
|
+
|
|
72
|
+
In `lib/gempilot/version_tag.rb`, in `assert_last_commit_is_bump!` (line 50), change:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
abort "Last commit does not appear to be a version bump." unless message.start_with?("Bump version to ")
|
|
76
|
+
```
|
|
77
|
+
to:
|
|
78
|
+
```ruby
|
|
79
|
+
raise "Last commit does not appear to be a version bump." unless message.start_with?("Bump version to ")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- [ ] **Step 4: Run the spec, verify it passes**
|
|
83
|
+
|
|
84
|
+
Run: `rspec spec/gempilot/version_tag_spec.rb`
|
|
85
|
+
Expected: PASS (all examples).
|
|
86
|
+
|
|
87
|
+
- [ ] **Step 5: Commit**
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
git add lib/gempilot/version_tag.rb spec/gempilot/version_tag_spec.rb
|
|
91
|
+
git commit -m "Raise instead of abort in version bump guard"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Task A2: Extract the bump-message prefix into a named constant
|
|
95
|
+
|
|
96
|
+
**Files:**
|
|
97
|
+
- Modify: `lib/gempilot/version_tag.rb`
|
|
98
|
+
|
|
99
|
+
- [ ] **Step 1: Confirm current behavior is covered, run the spec**
|
|
100
|
+
|
|
101
|
+
Run: `rspec spec/gempilot/version_tag_spec.rb`
|
|
102
|
+
Expected: PASS. (`#create` asserts the commit message; the guard test asserts the prefix check — both cover this refactor.)
|
|
103
|
+
|
|
104
|
+
- [ ] **Step 2: Add the constant and use it in both places**
|
|
105
|
+
|
|
106
|
+
In `lib/gempilot/version_tag.rb`, add the constant just below `include StrictShell`:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
include StrictShell
|
|
110
|
+
|
|
111
|
+
## Commit-message prefix written for a version bump; the guard below
|
|
112
|
+
## matches on it, so the two must stay in sync.
|
|
113
|
+
BUMP_MESSAGE_PREFIX = "Bump version to ".freeze
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
In `#create`, change the commit line to:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
sh "git", "commit", "-m", "#{BUMP_MESSAGE_PREFIX}#{version.value}"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
In `assert_last_commit_is_bump!`, change the guard to:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
raise "Last commit does not appear to be a version bump." unless message.start_with?(BUMP_MESSAGE_PREFIX)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- [ ] **Step 3: Run the spec, verify still green**
|
|
129
|
+
|
|
130
|
+
Run: `rspec spec/gempilot/version_tag_spec.rb`
|
|
131
|
+
Expected: PASS (commit message is byte-identical: `"Bump version to <value>"`).
|
|
132
|
+
|
|
133
|
+
- [ ] **Step 4: Lint and commit**
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
rubocop lib/gempilot/version_tag.rb
|
|
137
|
+
git add lib/gempilot/version_tag.rb
|
|
138
|
+
git commit -m "Name the version bump commit-message prefix"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Part B — `GemConstant` value object + de-duplicate new/destroy
|
|
144
|
+
|
|
145
|
+
### Task B1: Create the `GemConstant` value object
|
|
146
|
+
|
|
147
|
+
**Files:**
|
|
148
|
+
- Create: `lib/gempilot/gem_constant.rb`
|
|
149
|
+
- Test: `spec/gempilot/gem_constant_spec.rb`
|
|
150
|
+
|
|
151
|
+
- [ ] **Step 1: Write the unit spec**
|
|
152
|
+
|
|
153
|
+
Create `spec/gempilot/gem_constant_spec.rb`:
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
require "spec_helper"
|
|
157
|
+
|
|
158
|
+
RSpec.describe Gempilot::GemConstant do
|
|
159
|
+
def constant(input, gem_module: "MyGem", require_path: "my_gem")
|
|
160
|
+
described_class.new(input: input, gem_module: gem_module, require_path: require_path)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
describe "#qualified" do
|
|
164
|
+
it "prepends the gem module to a bare suffix" do
|
|
165
|
+
expect(constant("Services::Auth").qualified).to eq("MyGem::Services::Auth")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it "leaves an already-rooted constant unchanged" do
|
|
169
|
+
expect(constant("MyGem::Services::Auth").qualified).to eq("MyGem::Services::Auth")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it "matches on the root segment for multi-segment modules" do
|
|
173
|
+
c = constant("My::Widget", gem_module: "My::Gem", require_path: "my/gem")
|
|
174
|
+
expect(c.qualified).to eq("My::Widget")
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it "prepends the full module to a bare suffix under a multi-segment module" do
|
|
178
|
+
c = constant("Widget", gem_module: "My::Gem", require_path: "my/gem")
|
|
179
|
+
expect(c.qualified).to eq("My::Gem::Widget")
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
describe "#namespaces / #name" do
|
|
184
|
+
it "splits the qualified constant", :aggregate_failures do
|
|
185
|
+
c = constant("Services::Auth")
|
|
186
|
+
expect(c.namespaces).to eq(%w[MyGem Services])
|
|
187
|
+
expect(c.name).to eq("Auth")
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
describe "#lib_path" do
|
|
192
|
+
it "builds the underscored source path" do
|
|
193
|
+
expect(constant("Services::Auth").lib_path).to eq("lib/my_gem/services/auth.rb")
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
describe "#test_path" do
|
|
198
|
+
it "builds the rspec path" do
|
|
199
|
+
expect(constant("Services::Auth").test_path(:rspec)).to eq("spec/my_gem/services/auth_spec.rb")
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
it "builds the minitest path" do
|
|
203
|
+
expect(constant("Services::Auth").test_path(:minitest)).to eq("test/my_gem/services/auth_test.rb")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it "does not duplicate segments for multi-segment (hyphenated) modules" do
|
|
207
|
+
c = constant("Widget", gem_module: "My::Gem", require_path: "my/gem")
|
|
208
|
+
expect(c.test_path(:minitest)).to eq("test/my/gem/widget_test.rb")
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
- [ ] **Step 2: Run the spec, verify it fails**
|
|
215
|
+
|
|
216
|
+
Run: `rspec spec/gempilot/gem_constant_spec.rb`
|
|
217
|
+
Expected: FAIL — `uninitialized constant Gempilot::GemConstant`.
|
|
218
|
+
|
|
219
|
+
- [ ] **Step 3: Create the value object**
|
|
220
|
+
|
|
221
|
+
Create `lib/gempilot/gem_constant.rb`:
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
module Gempilot
|
|
225
|
+
## A class or module constant resolved within a gem's namespace.
|
|
226
|
+
##
|
|
227
|
+
## Wraps raw user input (a bare suffix like +Services::Auth+ or a
|
|
228
|
+
## fully-qualified +MyGem::Services::Auth+) together with the gem's module
|
|
229
|
+
## and require path, and derives the qualified constant, file paths, and
|
|
230
|
+
## namespace pieces from a single parse.
|
|
231
|
+
##
|
|
232
|
+
## Every constant is rooted at the gem's module: bare input is prefixed with
|
|
233
|
+
## it, while input already starting with the gem's root segment is left as
|
|
234
|
+
## is. Rooting is matched on the first segment only, so an extension gem
|
|
235
|
+
## whose module is +My::Gem+ accepts any +My::...+ constant.
|
|
236
|
+
class GemConstant < Data.define(:input, :gem_module, :require_path)
|
|
237
|
+
using String::Inflectable
|
|
238
|
+
|
|
239
|
+
## The fully-qualified constant, rooted at the gem module.
|
|
240
|
+
def qualified
|
|
241
|
+
input.start_with?("#{root_segment}::") ? input : "#{gem_module}::#{input}"
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
## Namespace segments preceding the final constant name.
|
|
245
|
+
def namespaces
|
|
246
|
+
parts[0...-1]
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
## The final class or module name.
|
|
250
|
+
def name
|
|
251
|
+
parts.last
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
## Path to the constant's source file, e.g. +lib/my_gem/services/auth.rb+.
|
|
255
|
+
def lib_path
|
|
256
|
+
"#{File.join("lib", *path_segments)}.rb"
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
## Path to the constant's test file for +framework+ (+:rspec+ or
|
|
260
|
+
## +:minitest+); correct for multi-segment (hyphenated) gem modules.
|
|
261
|
+
def test_path(framework)
|
|
262
|
+
rest = path_segments.drop(require_path.split("/").length)
|
|
263
|
+
if framework == :rspec
|
|
264
|
+
"#{File.join("spec", require_path, *rest)}_spec.rb"
|
|
265
|
+
else
|
|
266
|
+
"#{File.join("test", require_path, *rest)}_test.rb"
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
private
|
|
271
|
+
|
|
272
|
+
def root_segment
|
|
273
|
+
gem_module.split("::").first
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def parts
|
|
277
|
+
qualified.split("::")
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def path_segments
|
|
281
|
+
parts.map(&:underscore)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
- [ ] **Step 4: Run the spec, verify it passes**
|
|
288
|
+
|
|
289
|
+
Run: `rspec spec/gempilot/gem_constant_spec.rb`
|
|
290
|
+
Expected: PASS (all examples). If you see `undefined method 'underscore'`, confirm `String::Inflectable` is loaded at boot (it is for `new.rb`); the file-top `using` activates it for the whole file.
|
|
291
|
+
|
|
292
|
+
- [ ] **Step 5: Lint and commit**
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
rubocop lib/gempilot/gem_constant.rb spec/gempilot/gem_constant_spec.rb
|
|
296
|
+
git add lib/gempilot/gem_constant.rb spec/gempilot/gem_constant_spec.rb
|
|
297
|
+
git commit -m "Add GemConstant value object for gem-rooted constants"
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Task B2: Add a `gem_constant` factory to `GemContext`
|
|
301
|
+
|
|
302
|
+
**Files:**
|
|
303
|
+
- Modify: `lib/gempilot/cli/gem_context.rb`
|
|
304
|
+
|
|
305
|
+
- [ ] **Step 1: Add the factory method** (additive; `parse_constant`/`validate_gem_root!` stay for now so nothing breaks)
|
|
306
|
+
|
|
307
|
+
In `lib/gempilot/cli/gem_context.rb`, add this method inside `module GemContext` (after `detect_gem_context`):
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
def gem_constant(input)
|
|
311
|
+
GemConstant.new(input: input, gem_module: @gem_module, require_path: @require_path)
|
|
312
|
+
end
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
- [ ] **Step 2: Verify nothing is broken**
|
|
316
|
+
|
|
317
|
+
Run: `rspec`
|
|
318
|
+
Expected: PASS (additive change; existing suite unaffected).
|
|
319
|
+
|
|
320
|
+
- [ ] **Step 3: Commit**
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
git add lib/gempilot/cli/gem_context.rb
|
|
324
|
+
git commit -m "Add GemContext#gem_constant factory"
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Task B3: Refactor `new` to use `GemConstant`
|
|
328
|
+
|
|
329
|
+
**Files:**
|
|
330
|
+
- Modify: `lib/gempilot/cli/commands/new.rb`
|
|
331
|
+
- Test: `spec/gempilot/cli/commands/new_namespace_spec.rb`
|
|
332
|
+
|
|
333
|
+
- [ ] **Step 1: Add a spec for unified non-interactive rooting** (documents the now-consistent behavior: a bare name passed as an argument is rooted, instead of being rejected)
|
|
334
|
+
|
|
335
|
+
In `spec/gempilot/cli/commands/new_namespace_spec.rb`, add this context inside the top-level `describe` (the file already has the tmpdir `around` + `include FileUtils` + `my_gem` gemspec fixture):
|
|
336
|
+
|
|
337
|
+
```ruby
|
|
338
|
+
describe "non-interactive rooting" do
|
|
339
|
+
before { mkdir_p("lib/my_gem") }
|
|
340
|
+
|
|
341
|
+
it "roots a bare class name under the gem module" do
|
|
342
|
+
command.main(["class", "Services::Auth"])
|
|
343
|
+
|
|
344
|
+
expect(File).to exist("lib/my_gem/services/auth.rb")
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
- [ ] **Step 2: Run it, verify it fails**
|
|
350
|
+
|
|
351
|
+
Run: `rspec spec/gempilot/cli/commands/new_namespace_spec.rb -e "roots a bare class name"`
|
|
352
|
+
Expected: FAIL — current code calls `validate_gem_root!("Services")` and exits, so the file is not created (`SystemExit`).
|
|
353
|
+
|
|
354
|
+
- [ ] **Step 3: Refactor `new.rb`**
|
|
355
|
+
|
|
356
|
+
In `lib/gempilot/cli/commands/new.rb`:
|
|
357
|
+
|
|
358
|
+
Replace `prompt_for_path` (it no longer prepends — `GemConstant#qualified` owns that):
|
|
359
|
+
|
|
360
|
+
```ruby
|
|
361
|
+
def prompt_for_path(type)
|
|
362
|
+
ask(colors.green(type), required: true)
|
|
363
|
+
end
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Replace `dispatch_add` so class/module build a `GemConstant`:
|
|
367
|
+
|
|
368
|
+
```ruby
|
|
369
|
+
def dispatch_add(type, path)
|
|
370
|
+
case type
|
|
371
|
+
when "class" then add_class(gem_constant(path))
|
|
372
|
+
when "module" then add_module(gem_constant(path))
|
|
373
|
+
when "command" then add_command(path)
|
|
374
|
+
else
|
|
375
|
+
puts colors.red("Unknown type '#{type}'. Use class, module, or command.")
|
|
376
|
+
exit 1
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Delete `prepare_constant` and `class_test_path` entirely. Replace `add_class`, `add_module`, and `add_test_file` with:
|
|
382
|
+
|
|
383
|
+
```ruby
|
|
384
|
+
def add_class(constant)
|
|
385
|
+
print_adding_banner("class", constant.qualified)
|
|
386
|
+
ensure_directory(File.dirname(constant.lib_path))
|
|
387
|
+
source = build_nested_source(constant.namespaces, "class", constant.name)
|
|
388
|
+
create_file(constant.lib_path, source)
|
|
389
|
+
add_test_file(constant)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def add_module(constant)
|
|
393
|
+
print_adding_banner("module", constant.qualified)
|
|
394
|
+
ensure_directory(File.dirname(constant.lib_path))
|
|
395
|
+
source = build_nested_source(constant.namespaces, "module", constant.name)
|
|
396
|
+
create_file(constant.lib_path, source)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def add_test_file(constant)
|
|
400
|
+
test_path = constant.test_path(@test_framework)
|
|
401
|
+
ensure_directory(File.dirname(test_path))
|
|
402
|
+
content = if @test_framework == :rspec
|
|
403
|
+
rspec_class_content(constant.namespaces, constant.name)
|
|
404
|
+
else
|
|
405
|
+
minitest_class_content(constant.namespaces, constant.name)
|
|
406
|
+
end
|
|
407
|
+
create_file(test_path, content)
|
|
408
|
+
end
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Leave `add_command`, `command_test_path`, `add_command_test_file`, `rspec_class_content`, `minitest_class_content`, `rspec_command_content`, `minitest_command_content`, `build_nested_source`, `build_namespace_lines`, `build_closing_lines`, `print_adding_banner`, and `ensure_directory` unchanged.
|
|
412
|
+
|
|
413
|
+
- [ ] **Step 4: Run the new test, the command specs, and the minitest suite**
|
|
414
|
+
|
|
415
|
+
Run: `rspec spec/gempilot/cli/commands/new_namespace_spec.rb spec/gempilot/cli/commands/new_interactive_spec.rb`
|
|
416
|
+
Expected: PASS (interactive + hyphenated cases still green; new rooting test green).
|
|
417
|
+
|
|
418
|
+
Run: `ruby -Itest -Ilib -e 'require File.expand_path("test/gempilot/cli/new_command_test.rb")'`
|
|
419
|
+
Expected: PASS, 0 failures (full constants are unchanged by `qualified`).
|
|
420
|
+
|
|
421
|
+
- [ ] **Step 5: Lint and commit**
|
|
422
|
+
|
|
423
|
+
```bash
|
|
424
|
+
rubocop lib/gempilot/cli/commands/new.rb spec/gempilot/cli/commands/new_namespace_spec.rb
|
|
425
|
+
git add lib/gempilot/cli/commands/new.rb spec/gempilot/cli/commands/new_namespace_spec.rb
|
|
426
|
+
git commit -m "Refactor new command onto GemConstant"
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Task B4: Refactor `destroy` to use `GemConstant`
|
|
430
|
+
|
|
431
|
+
**Files:**
|
|
432
|
+
- Modify: `lib/gempilot/cli/commands/destroy.rb`
|
|
433
|
+
|
|
434
|
+
- [ ] **Step 1: Run the destroy specs (baseline green)**
|
|
435
|
+
|
|
436
|
+
Run: `rspec spec/gempilot/cli/commands/destroy_namespace_spec.rb spec/gempilot/cli/commands/destroy_interactive_spec.rb`
|
|
437
|
+
Expected: PASS.
|
|
438
|
+
|
|
439
|
+
- [ ] **Step 2: Refactor `destroy.rb`**
|
|
440
|
+
|
|
441
|
+
In `lib/gempilot/cli/commands/destroy.rb`:
|
|
442
|
+
|
|
443
|
+
Replace `prompt_for_path`:
|
|
444
|
+
|
|
445
|
+
```ruby
|
|
446
|
+
def prompt_for_path(type)
|
|
447
|
+
ask(colors.green(type), required: true)
|
|
448
|
+
end
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Replace `dispatch_destroy`:
|
|
452
|
+
|
|
453
|
+
```ruby
|
|
454
|
+
def dispatch_destroy(type, path)
|
|
455
|
+
case type
|
|
456
|
+
when "class" then destroy_class(gem_constant(path))
|
|
457
|
+
when "module" then destroy_module(gem_constant(path))
|
|
458
|
+
when "command" then destroy_command(path)
|
|
459
|
+
else
|
|
460
|
+
puts colors.red("Unknown type '#{type}'. Use class, module, or command.")
|
|
461
|
+
exit 1
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Delete `test_path_for`. Replace `destroy_class` and `destroy_module`:
|
|
467
|
+
|
|
468
|
+
```ruby
|
|
469
|
+
def destroy_class(constant)
|
|
470
|
+
lib_path = constant.lib_path
|
|
471
|
+
test_path = constant.test_path(@test_framework)
|
|
472
|
+
|
|
473
|
+
remove_file(lib_path)
|
|
474
|
+
remove_file(test_path)
|
|
475
|
+
cleanup_empty_dirs(lib_path, test_path)
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def destroy_module(constant)
|
|
479
|
+
lib_path = constant.lib_path
|
|
480
|
+
|
|
481
|
+
remove_file(lib_path)
|
|
482
|
+
remove_empty_parents(File.dirname(lib_path), File.join("lib", @require_path))
|
|
483
|
+
end
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
Leave `destroy_command`, `cleanup_empty_dirs`, `remove_empty_parents`, `remove_file`, `print_remove`, and `print_skip` unchanged.
|
|
487
|
+
|
|
488
|
+
- [ ] **Step 3: Run destroy specs + minitest**
|
|
489
|
+
|
|
490
|
+
Run: `rspec spec/gempilot/cli/commands/destroy_namespace_spec.rb spec/gempilot/cli/commands/destroy_interactive_spec.rb`
|
|
491
|
+
Expected: PASS.
|
|
492
|
+
|
|
493
|
+
Run: `ruby -Itest -Ilib -e 'require File.expand_path("test/gempilot/cli/destroy_command_test.rb")'`
|
|
494
|
+
Expected: PASS, 0 failures.
|
|
495
|
+
|
|
496
|
+
- [ ] **Step 4: Lint and commit**
|
|
497
|
+
|
|
498
|
+
```bash
|
|
499
|
+
rubocop lib/gempilot/cli/commands/destroy.rb
|
|
500
|
+
git add lib/gempilot/cli/commands/destroy.rb
|
|
501
|
+
git commit -m "Refactor destroy command onto GemConstant"
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### Task B5: Remove the now-dead `parse_constant` and `validate_gem_root!`
|
|
505
|
+
|
|
506
|
+
**Files:**
|
|
507
|
+
- Modify: `lib/gempilot/cli/gem_context.rb`
|
|
508
|
+
|
|
509
|
+
- [ ] **Step 1: Confirm they're unused**
|
|
510
|
+
|
|
511
|
+
Run: `grep -rn "parse_constant\|validate_gem_root" lib`
|
|
512
|
+
Expected: only the two definitions in `gem_context.rb` (no call sites remain after B3/B4).
|
|
513
|
+
|
|
514
|
+
- [ ] **Step 2: Delete both methods**
|
|
515
|
+
|
|
516
|
+
In `lib/gempilot/cli/gem_context.rb`, delete the entire `parse_constant` method and the entire `validate_gem_root!` method. Keep `using String::Inflectable` (still used by `detect_gem_context`'s `camelize`), `detect_gem_context`, and `gem_constant`.
|
|
517
|
+
|
|
518
|
+
- [ ] **Step 3: Run the full suite + minitest**
|
|
519
|
+
|
|
520
|
+
Run: `rspec`
|
|
521
|
+
Expected: PASS (whole RSpec suite).
|
|
522
|
+
|
|
523
|
+
Run: `ruby -Itest -Ilib -e 'Dir["test/**/*_test.rb"].each { |f| require File.expand_path(f) }'`
|
|
524
|
+
Expected: only the known environmental `create_command_test` bundler-subprocess failure (see memory `gempilot-test-running-env`); 0 other failures.
|
|
525
|
+
|
|
526
|
+
- [ ] **Step 4: Lint and commit**
|
|
527
|
+
|
|
528
|
+
```bash
|
|
529
|
+
rubocop lib/gempilot/cli/gem_context.rb
|
|
530
|
+
git add lib/gempilot/cli/gem_context.rb
|
|
531
|
+
git commit -m "Drop parse_constant/validate_gem_root! superseded by GemConstant"
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
|
|
536
|
+
## Part C — Robust test-framework detection
|
|
537
|
+
|
|
538
|
+
### Task C1: Detect rspec by its config files, not a bare `spec/` directory
|
|
539
|
+
|
|
540
|
+
**Files:**
|
|
541
|
+
- Modify: `lib/gempilot/cli/gem_context.rb`
|
|
542
|
+
- Test: `spec/gempilot/cli/commands/new_interactive_spec.rb`
|
|
543
|
+
|
|
544
|
+
- [ ] **Step 1: Add a spec proving a stray `spec/` dir doesn't force rspec**
|
|
545
|
+
|
|
546
|
+
In `spec/gempilot/cli/commands/new_interactive_spec.rb`, add inside `describe "interactive mode"`:
|
|
547
|
+
|
|
548
|
+
```ruby
|
|
549
|
+
context "when a spec/ directory exists but there is no rspec config" do
|
|
550
|
+
before { mkdir_p("spec") }
|
|
551
|
+
|
|
552
|
+
it "still scaffolds a minitest test file" do
|
|
553
|
+
generate("1\nServices::Auth\n")
|
|
554
|
+
|
|
555
|
+
expect(File).to exist("test/my_gem/services/auth_test.rb")
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
- [ ] **Step 2: Run it, verify it fails**
|
|
561
|
+
|
|
562
|
+
Run: `rspec spec/gempilot/cli/commands/new_interactive_spec.rb -e "stray"` (or `-e "no rspec config"`)
|
|
563
|
+
Expected: FAIL — current `File.directory?("spec")` returns true, so an rspec spec file is generated under `spec/` and the expected `test/...` file does not exist.
|
|
564
|
+
|
|
565
|
+
- [ ] **Step 3: Replace the detection**
|
|
566
|
+
|
|
567
|
+
In `lib/gempilot/cli/gem_context.rb`, change the last line of `detect_gem_context` from:
|
|
568
|
+
|
|
569
|
+
```ruby
|
|
570
|
+
@test_framework = File.directory?("spec") ? :rspec : :minitest
|
|
571
|
+
```
|
|
572
|
+
to:
|
|
573
|
+
```ruby
|
|
574
|
+
@test_framework = detect_test_framework
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
Add this private method (next to `gem_constant`):
|
|
578
|
+
|
|
579
|
+
```ruby
|
|
580
|
+
# Detect rspec by its canonical config files rather than the mere
|
|
581
|
+
# presence of a spec/ directory, which a minitest project may also have.
|
|
582
|
+
def detect_test_framework
|
|
583
|
+
return :rspec if File.exist?(".rspec") || File.exist?(File.join("spec", "spec_helper.rb"))
|
|
584
|
+
|
|
585
|
+
:minitest
|
|
586
|
+
end
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
- [ ] **Step 4: Run the spec + full suite**
|
|
590
|
+
|
|
591
|
+
Run: `rspec spec/gempilot/cli/commands/new_interactive_spec.rb`
|
|
592
|
+
Expected: PASS.
|
|
593
|
+
|
|
594
|
+
Run: `rspec`
|
|
595
|
+
Expected: PASS (whole suite).
|
|
596
|
+
|
|
597
|
+
- [ ] **Step 5: Lint and commit**
|
|
598
|
+
|
|
599
|
+
```bash
|
|
600
|
+
rubocop lib/gempilot/cli/gem_context.rb spec/gempilot/cli/commands/new_interactive_spec.rb
|
|
601
|
+
git add lib/gempilot/cli/gem_context.rb spec/gempilot/cli/commands/new_interactive_spec.rb
|
|
602
|
+
git commit -m "Detect rspec by config files, not a bare spec/ dir"
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
---
|
|
606
|
+
|
|
607
|
+
## Part D — CLAUDE.md accuracy pass
|
|
608
|
+
|
|
609
|
+
### Task D1: Correct the stale architecture and feature notes
|
|
610
|
+
|
|
611
|
+
**Files:**
|
|
612
|
+
- Modify: `CLAUDE.md`
|
|
613
|
+
|
|
614
|
+
- [ ] **Step 1: Fix the version-tasks note**
|
|
615
|
+
|
|
616
|
+
In `CLAUDE.md`, under "Generated Gem Features", replace:
|
|
617
|
+
|
|
618
|
+
```
|
|
619
|
+
- Version management rake tasks in `rakelib/version.rake` (current, bump, commit, revert, release:full)
|
|
620
|
+
```
|
|
621
|
+
with:
|
|
622
|
+
```
|
|
623
|
+
- Version lifecycle rake tasks installed via `Gempilot::VersionTask.new` (a `Rake::TaskLib`): `version:current/bump/commit/tag/untag/reset/revert`, composite `version:release`/`version:unrelease`, and `version:github:release/unrelease/list`
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
- [ ] **Step 2: Fix the `exe/` note in the ISSUES section**
|
|
627
|
+
|
|
628
|
+
In `CLAUDE.md`, update issue 1 to reflect reality — `exe/gempilot` keeps a *conditional* `bundler/setup` (only when a sibling `Gemfile` exists), it was not removed. Replace the issue body with:
|
|
629
|
+
|
|
630
|
+
```
|
|
631
|
+
1. RESOLVED — `exe/gempilot` previously had an unconditional `ENV["BUNDLE_GEMFILE"]` + `require "bundler/setup"`, breaking `gem install` usage. It now guards both behind `if File.exist?(gemfile)` (the Ronin pattern), so installed gems skip Bundler while in-repo runs still use it.
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
- [ ] **Step 3: Note the constant model under Architecture**
|
|
635
|
+
|
|
636
|
+
In `CLAUDE.md`, under "Architecture", add a bullet:
|
|
637
|
+
|
|
638
|
+
```
|
|
639
|
+
- `GemConstant` value object (`lib/gempilot/gem_constant.rb`) owns constant→namespace/path resolution for `new`/`destroy`; constants are rooted at the gem module by construction
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
- [ ] **Step 4: Commit**
|
|
643
|
+
|
|
644
|
+
```bash
|
|
645
|
+
git add CLAUDE.md
|
|
646
|
+
git commit -m "Refresh CLAUDE.md: version tasks, exe note, GemConstant"
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
---
|
|
650
|
+
|
|
651
|
+
## Final verification
|
|
652
|
+
|
|
653
|
+
- [ ] **Full RSpec suite**
|
|
654
|
+
|
|
655
|
+
Run: `rspec`
|
|
656
|
+
Expected: PASS (all examples, including the new `gem_constant_spec` and added cases).
|
|
657
|
+
|
|
658
|
+
- [ ] **Minitest suite**
|
|
659
|
+
|
|
660
|
+
Run: `ruby -Itest -Ilib -e 'Dir["test/**/*_test.rb"].each { |f| require File.expand_path(f) }'`
|
|
661
|
+
Expected: only the known environmental `create_command_test` bundler failure; 0 others.
|
|
662
|
+
|
|
663
|
+
- [ ] **RuboCop (real source dirs)**
|
|
664
|
+
|
|
665
|
+
Run: `rubocop lib spec test exe Rakefile`
|
|
666
|
+
Expected: no offenses.
|
|
667
|
+
|
|
668
|
+
- [ ] **Confirm net code reduction**
|
|
669
|
+
|
|
670
|
+
Run: `git diff --stat master -- lib/gempilot/cli/commands/new.rb lib/gempilot/cli/commands/destroy.rb lib/gempilot/cli/gem_context.rb`
|
|
671
|
+
Expected: `new.rb`/`destroy.rb`/`gem_context.rb` shrink (constant logic centralized in `gem_constant.rb`).
|
|
672
|
+
|
|
673
|
+
---
|
|
674
|
+
|
|
675
|
+
## Self-review notes (done while writing)
|
|
676
|
+
|
|
677
|
+
- **Spec coverage:** each critique maps to a task — duplication + bug class → B1–B5; `abort`/`raise` → A1; magic string → A2; fragile framework detection → C1; CLAUDE.md drift → D1. The `validate_gem_root!` leniency is resolved as a documented design decision in B1.
|
|
678
|
+
- **Type consistency:** `GemConstant#qualified/namespaces/name/lib_path/test_path(framework)` are used with the same names/signatures across B3/B4; the factory is `gem_constant(input)` everywhere.
|
|
679
|
+
- **Behavior change called out:** B3 makes non-interactive bare names root under the gem (previously rejected). No existing test asserted the rejection; B3 Step 1 adds a test documenting the new behavior.
|
|
@@ -27,7 +27,7 @@ module Gempilot
|
|
|
27
27
|
def run(type = nil, path = nil)
|
|
28
28
|
type ||= prompt_for_type
|
|
29
29
|
detect_gem_context
|
|
30
|
-
path ||= prompt_for_path
|
|
30
|
+
path ||= prompt_for_path(type)
|
|
31
31
|
dispatch_destroy(type, path)
|
|
32
32
|
end
|
|
33
33
|
|
|
@@ -38,16 +38,14 @@ module Gempilot
|
|
|
38
38
|
ask_multiple_choice(colors.green("Type"), %w[class module command])
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
def prompt_for_path
|
|
42
|
-
|
|
43
|
-
puts colors.bright_black("Fully-qualified constant name (e.g., #{@gem_module}::Services::Authentication).")
|
|
44
|
-
ask(colors.green("Constant"), required: true)
|
|
41
|
+
def prompt_for_path(type)
|
|
42
|
+
ask(colors.green(type), required: true)
|
|
45
43
|
end
|
|
46
44
|
|
|
47
45
|
def dispatch_destroy(type, path)
|
|
48
46
|
case type
|
|
49
|
-
when "class" then destroy_class(path)
|
|
50
|
-
when "module" then destroy_module(path)
|
|
47
|
+
when "class" then destroy_class(gem_constant(path))
|
|
48
|
+
when "module" then destroy_module(gem_constant(path))
|
|
51
49
|
when "command" then destroy_command(path)
|
|
52
50
|
else
|
|
53
51
|
puts colors.red("Unknown type '#{type}'. Use class, module, or command.")
|
|
@@ -56,25 +54,14 @@ module Gempilot
|
|
|
56
54
|
end
|
|
57
55
|
|
|
58
56
|
def destroy_class(constant)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
lib_path = "#{File.join("lib", *segments)}.rb"
|
|
63
|
-
test_path = test_path_for(segments)
|
|
57
|
+
lib_path = constant.lib_path
|
|
58
|
+
test_path = constant.test_path(@test_framework)
|
|
64
59
|
|
|
65
60
|
remove_file(lib_path)
|
|
66
61
|
remove_file(test_path)
|
|
67
62
|
cleanup_empty_dirs(lib_path, test_path)
|
|
68
63
|
end
|
|
69
64
|
|
|
70
|
-
def test_path_for(segments)
|
|
71
|
-
if @test_framework == :rspec
|
|
72
|
-
"#{File.join("spec", @require_path, *segments[1..])}_spec.rb"
|
|
73
|
-
else
|
|
74
|
-
"#{File.join("test", @require_path, *segments[1..])}_test.rb"
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
65
|
def cleanup_empty_dirs(lib_path, test_path)
|
|
79
66
|
remove_empty_parents(File.dirname(lib_path), File.join("lib", @require_path))
|
|
80
67
|
test_root = @test_framework == :rspec ? File.join("spec", @require_path) : File.join("test", @require_path)
|
|
@@ -82,10 +69,8 @@ module Gempilot
|
|
|
82
69
|
end
|
|
83
70
|
|
|
84
71
|
def destroy_module(constant)
|
|
85
|
-
|
|
86
|
-
validate_gem_root!(namespaces.first)
|
|
72
|
+
lib_path = constant.lib_path
|
|
87
73
|
|
|
88
|
-
lib_path = "#{File.join("lib", *segments)}.rb"
|
|
89
74
|
remove_file(lib_path)
|
|
90
75
|
remove_empty_parents(File.dirname(lib_path), File.join("lib", @require_path))
|
|
91
76
|
end
|
|
@@ -29,7 +29,7 @@ module Gempilot
|
|
|
29
29
|
def run(type = nil, path = nil)
|
|
30
30
|
type ||= prompt_for_type
|
|
31
31
|
detect_gem_context
|
|
32
|
-
path ||= prompt_for_path
|
|
32
|
+
path ||= prompt_for_path(type)
|
|
33
33
|
dispatch_add(type, path)
|
|
34
34
|
end
|
|
35
35
|
|
|
@@ -40,16 +40,14 @@ module Gempilot
|
|
|
40
40
|
ask_multiple_choice(colors.green("Type"), %w[class module command])
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
def prompt_for_path
|
|
44
|
-
|
|
45
|
-
puts colors.bright_black("Fully-qualified constant name (e.g., #{@gem_module}::Services::Authentication).")
|
|
46
|
-
ask(colors.green("Constant"), required: true)
|
|
43
|
+
def prompt_for_path(type)
|
|
44
|
+
ask(colors.green(type), required: true)
|
|
47
45
|
end
|
|
48
46
|
|
|
49
47
|
def dispatch_add(type, path)
|
|
50
48
|
case type
|
|
51
|
-
when "class" then add_class(path)
|
|
52
|
-
when "module" then add_module(path)
|
|
49
|
+
when "class" then add_class(gem_constant(path))
|
|
50
|
+
when "module" then add_module(gem_constant(path))
|
|
53
51
|
when "command" then add_command(path)
|
|
54
52
|
else
|
|
55
53
|
puts colors.red("Unknown type '#{type}'. Use class, module, or command.")
|
|
@@ -86,31 +84,23 @@ module Gempilot
|
|
|
86
84
|
puts
|
|
87
85
|
end
|
|
88
86
|
|
|
89
|
-
def prepare_constant(constant)
|
|
90
|
-
namespaces, name, segments = parse_constant(constant)
|
|
91
|
-
validate_gem_root!(namespaces.first)
|
|
92
|
-
file_path = "#{File.join("lib", *segments)}.rb"
|
|
93
|
-
ensure_directory(File.dirname(file_path))
|
|
94
|
-
[namespaces, name, segments, file_path]
|
|
95
|
-
end
|
|
96
|
-
|
|
97
87
|
def ensure_directory(dir)
|
|
98
88
|
mkdir(dir) unless File.directory?(dir)
|
|
99
89
|
end
|
|
100
90
|
|
|
101
91
|
def add_class(constant)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
source = build_nested_source(namespaces, "class",
|
|
105
|
-
create_file(
|
|
106
|
-
add_test_file(
|
|
92
|
+
print_adding_banner("class", constant.qualified)
|
|
93
|
+
ensure_directory(File.dirname(constant.lib_path))
|
|
94
|
+
source = build_nested_source(constant.namespaces, "class", constant.name)
|
|
95
|
+
create_file(constant.lib_path, source)
|
|
96
|
+
add_test_file(constant)
|
|
107
97
|
end
|
|
108
98
|
|
|
109
99
|
def add_module(constant)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
source = build_nested_source(namespaces, "module",
|
|
113
|
-
create_file(
|
|
100
|
+
print_adding_banner("module", constant.qualified)
|
|
101
|
+
ensure_directory(File.dirname(constant.lib_path))
|
|
102
|
+
source = build_nested_source(constant.namespaces, "module", constant.name)
|
|
103
|
+
create_file(constant.lib_path, source)
|
|
114
104
|
end
|
|
115
105
|
|
|
116
106
|
def add_command(name)
|
|
@@ -179,14 +169,6 @@ module Gempilot
|
|
|
179
169
|
create_file(test_path, content)
|
|
180
170
|
end
|
|
181
171
|
|
|
182
|
-
def class_test_path(segments)
|
|
183
|
-
if @test_framework == :rspec
|
|
184
|
-
"#{File.join("spec", @require_path, *segments[1..])}_spec.rb"
|
|
185
|
-
else
|
|
186
|
-
"#{File.join("test", @require_path, *segments[1..])}_test.rb"
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
|
|
190
172
|
def rspec_class_content(namespaces, class_name)
|
|
191
173
|
<<~RUBY
|
|
192
174
|
require "spec_helper"
|
|
@@ -211,13 +193,13 @@ module Gempilot
|
|
|
211
193
|
RUBY
|
|
212
194
|
end
|
|
213
195
|
|
|
214
|
-
def add_test_file(
|
|
215
|
-
test_path =
|
|
196
|
+
def add_test_file(constant)
|
|
197
|
+
test_path = constant.test_path(@test_framework)
|
|
216
198
|
ensure_directory(File.dirname(test_path))
|
|
217
199
|
content = if @test_framework == :rspec
|
|
218
|
-
rspec_class_content(namespaces,
|
|
200
|
+
rspec_class_content(constant.namespaces, constant.name)
|
|
219
201
|
else
|
|
220
|
-
minitest_class_content(namespaces,
|
|
202
|
+
minitest_class_content(constant.namespaces, constant.name)
|
|
221
203
|
end
|
|
222
204
|
create_file(test_path, content)
|
|
223
205
|
end
|
|
@@ -17,23 +17,19 @@ module Gempilot
|
|
|
17
17
|
@gem_name = File.basename(gemspec, ".gemspec")
|
|
18
18
|
@require_path = @gem_name.tr("-", "/")
|
|
19
19
|
@gem_module = @require_path.camelize
|
|
20
|
-
@test_framework =
|
|
20
|
+
@test_framework = detect_test_framework
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
segments = parts.map(&:underscore)
|
|
28
|
-
[namespaces, name, segments]
|
|
29
|
-
end
|
|
23
|
+
# Detect rspec by its canonical config files rather than the mere
|
|
24
|
+
# presence of a spec/ directory, which a minitest project may also have.
|
|
25
|
+
def detect_test_framework
|
|
26
|
+
return :rspec if File.exist?(".rspec") || File.exist?(File.join("spec", "spec_helper.rb"))
|
|
30
27
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return if root == expected
|
|
28
|
+
:minitest
|
|
29
|
+
end
|
|
34
30
|
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
def gem_constant(input)
|
|
32
|
+
GemConstant.new(input: input, gem_module: @gem_module, require_path: @require_path)
|
|
37
33
|
end
|
|
38
34
|
end
|
|
39
35
|
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Gempilot
|
|
2
|
+
## A class or module constant resolved within a gem's namespace.
|
|
3
|
+
##
|
|
4
|
+
## Wraps raw user input (a bare suffix like +Services::Auth+ or a
|
|
5
|
+
## fully-qualified +MyGem::Services::Auth+) together with the gem's module
|
|
6
|
+
## and require path, and derives the qualified constant, file paths, and
|
|
7
|
+
## namespace pieces from a single parse.
|
|
8
|
+
##
|
|
9
|
+
## Every constant is rooted at the gem's module: bare input is prefixed with
|
|
10
|
+
## it, while input already starting with the gem's root segment is left as
|
|
11
|
+
## is. Rooting is matched on the first segment only, so an extension gem
|
|
12
|
+
## whose module is +My::Gem+ accepts any +My::...+ constant.
|
|
13
|
+
GemConstant = Data.define(:input, :gem_module, :require_path) do
|
|
14
|
+
using String::Inflectable
|
|
15
|
+
|
|
16
|
+
## The fully-qualified constant, rooted at the gem module.
|
|
17
|
+
def qualified
|
|
18
|
+
input.start_with?("#{root_segment}::") ? input : "#{gem_module}::#{input}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
## Namespace segments preceding the final constant name.
|
|
22
|
+
def namespaces
|
|
23
|
+
parts[0...-1]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
## The final class or module name.
|
|
27
|
+
def name
|
|
28
|
+
parts.last
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
## Path to the constant's source file, e.g. +lib/my_gem/services/auth.rb+.
|
|
32
|
+
def lib_path
|
|
33
|
+
"#{File.join("lib", *path_segments)}.rb"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
## Path to the constant's test file for +framework+ (+:rspec+ or
|
|
37
|
+
## +:minitest+); correct for multi-segment (hyphenated) gem modules.
|
|
38
|
+
def test_path(framework)
|
|
39
|
+
rest = path_segments.drop(require_path.split("/").length)
|
|
40
|
+
if framework == :rspec
|
|
41
|
+
"#{File.join("spec", require_path, *rest)}_spec.rb"
|
|
42
|
+
else
|
|
43
|
+
"#{File.join("test", require_path, *rest)}_test.rb"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def root_segment
|
|
50
|
+
gem_module.split("::").first
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def parts
|
|
54
|
+
qualified.split("::")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def path_segments
|
|
58
|
+
parts.map(&:underscore)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/gempilot/version.rb
CHANGED
data/lib/gempilot/version_tag.rb
CHANGED
|
@@ -5,6 +5,10 @@ module Gempilot
|
|
|
5
5
|
class VersionTag
|
|
6
6
|
include StrictShell
|
|
7
7
|
|
|
8
|
+
## Commit-message prefix written for a version bump; the guard below
|
|
9
|
+
## matches on it, so the two must stay in sync.
|
|
10
|
+
BUMP_MESSAGE_PREFIX = "Bump version to ".freeze
|
|
11
|
+
|
|
8
12
|
attr_reader :version
|
|
9
13
|
|
|
10
14
|
def initialize(version)
|
|
@@ -16,7 +20,7 @@ module Gempilot
|
|
|
16
20
|
raise "Cannot proceed, staging area must be clean" unless status.success?
|
|
17
21
|
|
|
18
22
|
sh "git", "add", version.path.to_s
|
|
19
|
-
sh "git", "commit", "-m", "
|
|
23
|
+
sh "git", "commit", "-m", "#{BUMP_MESSAGE_PREFIX}#{version.value}"
|
|
20
24
|
end
|
|
21
25
|
|
|
22
26
|
def tag
|
|
@@ -47,7 +51,7 @@ module Gempilot
|
|
|
47
51
|
raise "Failed to read last commit message" unless status.success?
|
|
48
52
|
|
|
49
53
|
message.strip!
|
|
50
|
-
|
|
54
|
+
raise "Last commit does not appear to be a version bump." unless message.start_with?(BUMP_MESSAGE_PREFIX)
|
|
51
55
|
end
|
|
52
56
|
end
|
|
53
57
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gempilot
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Gillis
|
|
@@ -114,6 +114,7 @@ files:
|
|
|
114
114
|
- docs/superpowers/plans/2026-04-06-inflection-tests-and-erb-rename.md
|
|
115
115
|
- docs/superpowers/plans/2026-04-06-integrate-version-tools.md
|
|
116
116
|
- docs/superpowers/plans/2026-04-06-new-readme.md
|
|
117
|
+
- docs/superpowers/plans/2026-06-09-address-review-critiques.md
|
|
117
118
|
- docs/version-management-redesign.md
|
|
118
119
|
- exe/gempilot
|
|
119
120
|
- issues.rec
|
|
@@ -131,6 +132,7 @@ files:
|
|
|
131
132
|
- lib/gempilot/cli/gem_builder.rb
|
|
132
133
|
- lib/gempilot/cli/gem_context.rb
|
|
133
134
|
- lib/gempilot/cli/generator.rb
|
|
135
|
+
- lib/gempilot/gem_constant.rb
|
|
134
136
|
- lib/gempilot/github_release.rb
|
|
135
137
|
- lib/gempilot/project.rb
|
|
136
138
|
- lib/gempilot/project/version.rb
|
|
@@ -159,7 +161,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
159
161
|
- !ruby/object:Gem::Version
|
|
160
162
|
version: '0'
|
|
161
163
|
requirements: []
|
|
162
|
-
rubygems_version: 4.0.
|
|
164
|
+
rubygems_version: 4.0.14
|
|
163
165
|
specification_version: 4
|
|
164
166
|
summary: A toolkit for creating, managing, and releasing your own rubygems
|
|
165
167
|
test_files: []
|