gem-contribute 0.1.0 → 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.
@@ -0,0 +1,425 @@
1
+ ---
2
+ marp: true
3
+ theme: default
4
+ paginate: true
5
+ size: 16:9
6
+ header: 'Blue Ridge Ruby 2026 · gem-contribute'
7
+ footer: ' '
8
+ style: |
9
+ /* Blue Ridge Ruby 2026 palette, pulled from the conference mark. */
10
+ :root {
11
+ --brr-light: #a8cce4; /* pale ridge blue */
12
+ --brr-ruby: #c2272e; /* ruby red */
13
+ --brr-blue: #2872b4; /* mid blue */
14
+ --brr-navy: #0e2854; /* deep navy */
15
+ --brr-cream: #fbf9f5; /* slide background */
16
+ --brr-ink: #16213a; /* body text */
17
+ }
18
+
19
+ section {
20
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", sans-serif;
21
+ background: var(--brr-cream);
22
+ color: var(--brr-ink);
23
+ padding: 60px 80px;
24
+ position: relative;
25
+ }
26
+
27
+ /* Header strap with the conference identifier on every content slide.
28
+ Uses Marp's `header:` directive so it survives PDF export (unlike
29
+ ::before/::after `content:` properties, which Marp drops). */
30
+ header {
31
+ position: absolute;
32
+ top: 28px; right: 80px;
33
+ left: auto;
34
+ width: auto;
35
+ margin: 0;
36
+ font-size: 0.65em;
37
+ letter-spacing: 0.08em;
38
+ text-transform: uppercase;
39
+ color: var(--brr-blue);
40
+ }
41
+
42
+ /* Footer is empty content but its element provides the gradient bar at
43
+ the bottom of every slide. The four-color stripe is the conference
44
+ palette in order. */
45
+ footer {
46
+ position: absolute;
47
+ left: 0; right: 0; bottom: 0;
48
+ margin: 0;
49
+ padding: 0;
50
+ height: 6px;
51
+ width: 100%;
52
+ color: transparent;
53
+ font-size: 0;
54
+ background: linear-gradient(
55
+ to right,
56
+ var(--brr-light) 0% 25%,
57
+ var(--brr-ruby) 25% 50%,
58
+ var(--brr-blue) 50% 75%,
59
+ var(--brr-navy) 75% 100%
60
+ );
61
+ }
62
+
63
+ h1, h2, h3 {
64
+ color: var(--brr-navy);
65
+ font-weight: 700;
66
+ letter-spacing: -0.01em;
67
+ }
68
+ h1 { font-size: 2.4em; }
69
+ h2 { font-size: 1.8em; }
70
+
71
+ a { color: var(--brr-blue); text-decoration: underline; }
72
+ strong { color: var(--brr-ruby); }
73
+
74
+ code, pre { font-family: "JetBrains Mono", "Fira Code", Menlo, monospace; }
75
+ code {
76
+ background: rgba(40, 114, 180, 0.10);
77
+ color: var(--brr-navy);
78
+ padding: 2px 6px;
79
+ border-radius: 3px;
80
+ }
81
+ pre {
82
+ font-size: 0.78em;
83
+ background: var(--brr-navy);
84
+ color: #f0f4fa;
85
+ padding: 18px 22px;
86
+ border-radius: 6px;
87
+ border-left: 4px solid var(--brr-ruby);
88
+ }
89
+ pre code {
90
+ background: transparent;
91
+ color: inherit;
92
+ padding: 0;
93
+ }
94
+
95
+ blockquote {
96
+ font-size: 1.5em;
97
+ border-left: 6px solid var(--brr-ruby);
98
+ padding: 8px 0 8px 28px;
99
+ margin-left: 0;
100
+ color: var(--brr-navy);
101
+ font-style: normal;
102
+ font-weight: 500;
103
+ }
104
+
105
+ table { border-collapse: collapse; }
106
+ th { background: var(--brr-navy); color: white; padding: 10px 16px; }
107
+ td { padding: 8px 16px; border-bottom: 1px solid var(--brr-light); }
108
+ tr:last-child td { border-bottom: none; }
109
+
110
+ .small { font-size: 0.7em; color: #5a6a82; }
111
+ .big { font-size: 1.6em; }
112
+
113
+ /* Page numbers, themed. */
114
+ section::part(pagination) { color: var(--brr-blue); }
115
+
116
+ /* Title and closing slides invert: navy background, light type.
117
+ All overrides below were chosen to clear WCAG AA (4.5:1) for normal
118
+ text and 3:1 for large text against the navy background. */
119
+ section.title {
120
+ background: var(--brr-navy);
121
+ color: white;
122
+ text-align: center;
123
+ padding-top: 18%;
124
+ }
125
+ /* On title and closing slides we hide the header strap (no `Blue Ridge
126
+ Ruby 2026 · gem-contribute` repeating over the title) but keep the
127
+ gradient bar — it visually anchors every slide identically. */
128
+ section.title header { display: none; }
129
+ section.title h1 {
130
+ color: white;
131
+ font-size: 3.2em;
132
+ margin-bottom: 0.1em;
133
+ }
134
+ section.title h2 {
135
+ color: var(--brr-light);
136
+ font-weight: 400;
137
+ font-size: 1.4em;
138
+ margin-top: 0;
139
+ }
140
+ /* Brighter ruby (#ff5b62 ≈ 5.0:1 on navy) so the URL pops without
141
+ dropping below WCAG AA. The original --brr-ruby is fine on cream
142
+ but fails on navy. */
143
+ section.title strong { color: #ff5b62; }
144
+ section.title a { color: var(--brr-light); }
145
+ /* Inline code on title slides: invert to white-on-translucent-white
146
+ (≈ 12:1 on navy) — the cream-on-cream default is invisible here. */
147
+ section.title code {
148
+ background: rgba(255, 255, 255, 0.18);
149
+ color: white;
150
+ }
151
+ section.title pre {
152
+ background: rgba(255, 255, 255, 0.08);
153
+ color: white;
154
+ border-left-color: #ff5b62;
155
+ }
156
+ section.title pre code {
157
+ background: transparent;
158
+ color: inherit;
159
+ }
160
+ /* Disclosure / fine-print on title slides — clears AA at ~6.5:1. */
161
+ section.title .small { color: #b9cde0; }
162
+ /* Blockquotes on title slides: white text, no left border (clashes
163
+ with center alignment), constrained width so lines wrap naturally. */
164
+ section.title blockquote {
165
+ color: white;
166
+ border-left: none;
167
+ text-align: center;
168
+ padding: 8px 0;
169
+ margin: 0 auto;
170
+ max-width: 70%;
171
+ font-size: 1.4em;
172
+ }
173
+ ---
174
+
175
+ <!-- _class: title -->
176
+
177
+ <!-- Speaker note (~10s): Title slide. Smile, take a breath, let
178
+ the room settle. "I'm Chris Hagmann. I want to talk about
179
+ building tools that don't exist yet — using a small one I
180
+ made this week as the example." -->
181
+
182
+ # gem-contribute
183
+
184
+ ## Building what you cannot find
185
+
186
+ Chris Hagmann · Blue Ridge Ruby 2026
187
+
188
+ ---
189
+
190
+ <!-- Speaker note (~15s): Hold the slide. Don't read it aloud —
191
+ let them read it themselves. Then, quietly: "That's been me
192
+ for years. I suspect it's been some of you." Beat. Move on.
193
+ Make it personal, not a claim about the whole room. -->
194
+
195
+ > I wanted to contribute back.
196
+ >
197
+ > I never figured out where to start.
198
+
199
+ ---
200
+
201
+ <!-- Speaker note (~25s): "I volunteered yesterday to help with
202
+ Hack Day tomorrow. When I said yes, I needed a list of good
203
+ Ruby projects with approachable issues to point people at.
204
+ That list didn't exist." Pause on "didn't exist."
205
+ Then: "So I built one. And along the way I noticed something
206
+ that I think generalizes." That's the bridge to the next
207
+ slide. -->
208
+
209
+ ## A small problem
210
+
211
+ I volunteered to help with **Hack Day** tomorrow.
212
+
213
+ <br>
214
+
215
+ I needed a list of **good Ruby projects** to point people at.
216
+
217
+ <br>
218
+
219
+ That list didn't exist.
220
+
221
+ <br>
222
+
223
+ > So I built one.
224
+
225
+ ---
226
+
227
+ <!-- Speaker note (~20s): "There ARE resources. Four of them, all
228
+ fine, all sparse." Don't read the URLs. The point is the
229
+ pattern, not the list. "They're sparse for the same reason:
230
+ they need a maintainer to opt their project in. Most
231
+ maintainers never do."
232
+ Then the turn: "The signal I needed was a different kind of
233
+ opt-in. Mine." Land on "mine." -->
234
+
235
+ ## What's out there
236
+
237
+ - **goodfirstissue.dev** · opt-in registry · sparse
238
+ - **goodfirstissues.com** · opt-in registry · sparse
239
+ - **github.com/topics/good-first-issue** · opt-in topic · sparse
240
+ - **forgoodfirstissue.github.com** · curated · narrow
241
+
242
+ <br>
243
+
244
+ These all rely on **maintainer opt-in** — and most maintainers never do.
245
+
246
+ <br>
247
+
248
+ The signal I needed was a different kind of opt-in: **mine.**
249
+
250
+ ---
251
+
252
+ <!-- Speaker note (~25s): "Bundler shipped this insight years
253
+ ago. `bundle fund` reads your Gemfile.lock to answer one
254
+ question: where should my dollars go? It's the right index
255
+ for the question." Beat. "Same index, different question:
256
+ where should my hours go?"
257
+ Land hard on the slogan. This is the meme of the talk. -->
258
+
259
+ ## `bundle fund` for time
260
+
261
+ `bundle fund` reads your `Gemfile.lock` to answer
262
+ *"where should my **dollars** go?"*
263
+
264
+ <br>
265
+
266
+ `gem-contribute` reads the same file to answer
267
+ *"where should my **hours** go?"*
268
+
269
+ ---
270
+
271
+ <!-- Speaker note (~20s): "Two hundred and sixteen gems in this
272
+ project. Two hundred and sixteen maintainers I've already
273
+ bet on. Two hundred and sixteen codebases I have at least
274
+ a little context on."
275
+ "That's already a curated list. I just had to use it." -->
276
+
277
+ ## The insight was already on disk
278
+
279
+ ```
280
+ $ bundle list | wc -l
281
+ 216
282
+ ```
283
+
284
+ <br>
285
+
286
+ Your `Gemfile.lock` is already curated.
287
+
288
+ - ~216 maintainers you've **already bet on**
289
+ - The OSS code you have **the most context on**
290
+ - A vote of confidence with versions attached
291
+
292
+ <br>
293
+
294
+ Start *there*, not on GitHub.
295
+
296
+ ---
297
+
298
+ <!-- Speaker note (~45s): Let them read the output for ~10s
299
+ before talking. Then walk down the list:
300
+ "Sorbet has fifty open good-first-issues. Fifty."
301
+ "RSpec OpenAPI, five. Packwerk, four. Rubocop, four."
302
+ Point at gem-contribute on row three. Smile.
303
+ "And the tool itself, four. It found itself. We'll come
304
+ back to that." That's the meta-joke; don't oversell it. -->
305
+
306
+ ## So I built it
307
+
308
+ ```
309
+ $ gem-contribute scan
310
+ Scanning Gemfile.lock (234 gems)...
311
+ 234 gems · 230 on github.com · 1 on gitlab.com · 3 unknown source
312
+
313
+ Top contributable projects (by open `good first issue` count):
314
+ sorbet-runtime 50 github.com/sorbet/sorbet
315
+ rspec-openapi 5 github.com/exoego/rspec-openapi
316
+ gem-contribute 4 github.com/cdhagmann/gem-contribute
317
+ packwerk 4 github.com/Shopify/packwerk
318
+ rubocop 4 github.com/rubocop/rubocop
319
+ gitlab 3 github.com/NARKOZ/gitlab
320
+ pundit 2 github.com/varvet/pundit
321
+ ...
322
+ ```
323
+
324
+ <!-- (Drill-in slide cut for time. Mention in patter:
325
+ "You can list the issues for any of these.") -->
326
+
327
+ ---
328
+
329
+ <!-- Speaker note (~40s): Pre-frame: "You pick an issue. One
330
+ command does the rest."
331
+ Read silence ~8s. Then narrate: "It forks the repo to your
332
+ account. Clones the fork locally. Adds the upstream remote.
333
+ Creates a branch named after the issue."
334
+ Pause. "All the git ceremony, gone." -->
335
+
336
+ ## Fix it
337
+
338
+ ```
339
+ $ gem-contribute fix rubocop/14102
340
+ Forking rubocop/rubocop → cdhagmann/rubocop...
341
+ Cloning into ~/code/oss/rubocop/rubocop...
342
+ Forked, cloned, and branched.
343
+ path: ~/code/oss/rubocop/rubocop
344
+ branch: gem-contribute/issue-14102
345
+ upstream: github.com/rubocop/rubocop
346
+ fork: github.com/cdhagmann/rubocop
347
+
348
+ Next: cd ~/code/oss/rubocop/rubocop && make your changes,
349
+ then `gem-contribute submit`.
350
+ ```
351
+
352
+ ---
353
+
354
+ <!-- Speaker note (~35s): "You write the fix. You commit it.
355
+ You run submit." Beat for the URL to render.
356
+ "It pushes your branch. It opens the compare URL with the
357
+ title, the body, and `Closes #14102` already filled in."
358
+ Bottom-line it: "Browser opens. PR is pre-filled. Review.
359
+ Click Create." -->
360
+
361
+ ## Submit it
362
+
363
+ ```
364
+ $ gem-contribute submit
365
+ Pushing gem-contribute/issue-14102 to origin...
366
+ Opened browser to:
367
+ https://github.com/rubocop/rubocop/compare/cdhagmann:gem-contribute/issue-14102
368
+ ?expand=1&title=Fix+%2314102%3A+Allow+Lint%2FVoid+...&body=Closes+%2314102.
369
+ ```
370
+
371
+ <br>
372
+
373
+ Browser opens. PR is pre-filled. Review. Click **Create**.
374
+
375
+ ---
376
+
377
+ <!-- Speaker note (~20s): The bridge from demo to lesson. Slow
378
+ down here. The tool is the example, not the lesson.
379
+ "I'm doing this for gems. But the pattern works wherever
380
+ you look. The things you depend on are the things you should
381
+ give back to. Once you see that, you start noticing it
382
+ everywhere." Pause. Move on. -->
383
+
384
+ ## The pattern generalizes
385
+
386
+ > The things you depend on
387
+ > are the things you should give back to.
388
+ >
389
+ > Once you see that, you start noticing it everywhere.
390
+
391
+ ---
392
+
393
+ <!-- _class: title -->
394
+
395
+ <!-- Speaker note (~15s): The takeaway slide. Read it slowly,
396
+ one beat per line. Don't editorialize. The audience should
397
+ leave the room with this in their head — not the install
398
+ command, not the gem name, this. -->
399
+
400
+ > Build for yourself first.
401
+ >
402
+ > Help where you can.
403
+ >
404
+ > The rest is bonus.
405
+
406
+ ---
407
+
408
+ <!-- _class: title -->
409
+
410
+ <!-- Speaker note (~10s): "Thanks. The gem installs today. I'll
411
+ be at Hack Day tomorrow if you want to try it on your
412
+ laptop. Find me." Don't run over. Step back from the mic. -->
413
+
414
+ ## Thanks
415
+
416
+ `gem install gem-contribute`
417
+ **cdhagmann.com/gem-contribute**
418
+
419
+ <br>
420
+
421
+ Find me at **Hack Day** tomorrow — I'll watch it work on your laptop.
422
+
423
+ <br>
424
+
425
+ <span class="small">AI-assisted; ADRs explain why.</span>
Binary file
@@ -18,8 +18,9 @@ module GemContribute
18
18
  list Print all configured values.
19
19
 
20
20
  Keys:
21
- clone_root Directory where forks are cloned (default: ~/code/oss).
22
- Example: gem-contribute config set clone_root ~/Projects/oss
21
+ clone_root Directory where forks are cloned. Set with
22
+ `gem-contribute init` (interactive) or
23
+ `gem-contribute config set clone_root <path>`.
23
24
  USAGE
24
25
 
25
26
  def initialize(stdout: $stdout, stderr: $stderr, config: GemContribute::Config.new)
@@ -73,13 +74,14 @@ module GemContribute
73
74
  return 1
74
75
  end
75
76
 
76
- @stdout.puts @config.to_h.fetch(key, "(not set default applies)")
77
+ @stdout.puts @config.to_h.fetch(key, "(not set; run `gem-contribute init`)")
77
78
  0
78
79
  end
79
80
 
80
81
  def list
81
82
  @stdout.puts "Configuration (#{GemContribute::Config.default_path}):"
82
- @stdout.puts " clone_root = #{@config.clone_root}"
83
+ display = @config.clone_root || "(not set; run `gem-contribute init`)"
84
+ @stdout.puts " clone_root = #{display}"
83
85
  0
84
86
  end
85
87
  end
@@ -48,6 +48,8 @@ module GemContribute
48
48
  end
49
49
 
50
50
  def run(argv)
51
+ return missing_clone_root if @clone_root.nil?
52
+
51
53
  target = argv.shift
52
54
  return print_usage_error if target.nil? || !target.include?("/")
53
55
 
@@ -69,6 +71,11 @@ module GemContribute
69
71
 
70
72
  private
71
73
 
74
+ def missing_clone_root
75
+ @stderr.puts "clone_root is not configured. Run `gem-contribute init` first."
76
+ 1
77
+ end
78
+
72
79
  def print_usage_error
73
80
  @stderr.puts "Usage: gem-contribute fork-clone-branch <gem>/<issue#>"
74
81
  2
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GemContribute
4
+ module CLI
5
+ # `gem-contribute init` — interactive one-time setup. Writes the user's
6
+ # `clone_root` to ~/.config/gem-contribute/config.yml and, if no GitHub
7
+ # token is cached, offers to run `auth login`.
8
+ #
9
+ # Without init, `fix` errors with a hint to run init. The point is to
10
+ # avoid creating directories or assuming auth without explicit consent.
11
+ class Init
12
+ USAGE = <<~USAGE
13
+ Usage: gem-contribute init
14
+
15
+ Interactively set the directory where forks are cloned (clone_root),
16
+ then offer to authenticate with GitHub if you haven't already.
17
+ Re-run any time to change.
18
+ USAGE
19
+
20
+ DEFAULT_SUGGESTION = "~/code/oss"
21
+ AUTH_HOST = "github.com"
22
+
23
+ def initialize(stdout: $stdout, stderr: $stderr,
24
+ config: GemContribute::Config.new,
25
+ store: GemContribute::TokenStore.new,
26
+ auth: nil,
27
+ gets: -> { $stdin.gets })
28
+ @stdout = stdout
29
+ @stderr = stderr
30
+ @config = config
31
+ @store = store
32
+ @auth = auth || GemContribute::CLI::Auth.new(stdout: stdout, stderr: stderr, store: store)
33
+ @gets = gets
34
+ end
35
+
36
+ def run(argv)
37
+ return print_usage if %w[help -h --help].include?(argv.first)
38
+
39
+ prompt_clone_root
40
+ maybe_authenticate
41
+ 0
42
+ end
43
+
44
+ private
45
+
46
+ def prompt_clone_root
47
+ current = @config.to_h["clone_root"]
48
+ default = current || DEFAULT_SUGGESTION
49
+
50
+ @stdout.print "Where should I clone repos? [#{default}]: "
51
+ @stdout.flush
52
+ input = @gets.call.to_s.chomp.strip
53
+ chosen = input.empty? ? default : input
54
+
55
+ @config.set("clone_root", chosen)
56
+ @stdout.puts "Clone root set to #{File.expand_path(chosen)}"
57
+ end
58
+
59
+ def maybe_authenticate
60
+ if @store.token_for(AUTH_HOST)
61
+ @stdout.puts "GitHub: already authenticated."
62
+ return
63
+ end
64
+
65
+ @stdout.print "Authenticate with GitHub now? [Y/n]: "
66
+ @stdout.flush
67
+ answer = @gets.call.to_s.chomp.strip.downcase
68
+
69
+ if %w[n no].include?(answer)
70
+ @stdout.puts "Skipping auth. Run `gem-contribute auth login` when you're ready."
71
+ return
72
+ end
73
+
74
+ @auth.run(["login"])
75
+ end
76
+
77
+ def print_usage
78
+ @stdout.puts USAGE
79
+ 0
80
+ end
81
+ end
82
+ end
83
+ end
@@ -27,14 +27,18 @@ module GemContribute
27
27
  target = argv.shift
28
28
  return print_usage if target.nil?
29
29
 
30
- if target == "all"
31
- run_all
32
- else
33
- project = resolve_or_fail(target)
34
- return 1 if project.nil?
35
-
36
- list_issues(project)
37
- end
30
+ status = if target == "all"
31
+ run_all
32
+ else
33
+ project = resolve_or_fail(target)
34
+ if project.nil?
35
+ 1
36
+ else
37
+ list_issues(project)
38
+ end
39
+ end
40
+ RateLimitFooter.print(adapter: @adapter, stdout: @stdout)
41
+ status
38
42
  rescue AdapterError => e
39
43
  @stderr.puts "gem-contribute: #{e.message}"
40
44
  1
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GemContribute
4
+ module CLI
5
+ # Prints a one-line GitHub rate-limit footer after `scan` or `issues`
6
+ # finishes its main output, when the adapter has rate-limit data.
7
+ #
8
+ # Format: "GitHub rate limit: 4,587 / 5,000 remaining · resets at 14:32 UTC"
9
+ #
10
+ # When `adapter.rate_limit` is nil (e.g. every call was served from cache),
11
+ # nothing is printed — see #4 acceptance criteria.
12
+ module RateLimitFooter
13
+ module_function
14
+
15
+ # @param adapter [GemContribute::HostAdapters::GitHubAdapter]
16
+ # @param stdout [IO]
17
+ def print(adapter:, stdout: $stdout)
18
+ rate_limit = adapter.respond_to?(:rate_limit) ? adapter.rate_limit : nil
19
+ return if rate_limit.nil?
20
+
21
+ remaining = format_with_separators(rate_limit.remaining)
22
+ limit = format_with_separators(rate_limit.limit)
23
+ reset = rate_limit.reset_at.utc.strftime("%H:%M")
24
+ stdout.puts "GitHub rate limit: #{remaining} / #{limit} remaining · resets at #{reset} UTC"
25
+ end
26
+
27
+ def format_with_separators(integer)
28
+ integer.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
29
+ end
30
+ end
31
+ end
32
+ end
@@ -40,6 +40,7 @@ module GemContribute
40
40
  # self-injection is intentionally additive, not part of the count.
41
41
  print_summary(tally_hosts(projects), gems.size)
42
42
  scan_github_projects(projects)
43
+ RateLimitFooter.print(adapter: @adapter, stdout: @stdout)
43
44
  0
44
45
  rescue LockfileNotFound => e
45
46
  @stderr.puts "gem-contribute: #{e.message}"
@@ -7,14 +7,17 @@ module GemContribute
7
7
  autoload :Scan, "gem_contribute/cli/scan"
8
8
  autoload :Auth, "gem_contribute/cli/auth"
9
9
  autoload :Config, "gem_contribute/cli/config"
10
+ autoload :Init, "gem_contribute/cli/init"
10
11
  autoload :Issues, "gem_contribute/cli/issues"
11
12
  autoload :ForkCloneBranch, "gem_contribute/cli/fork_clone_branch"
12
13
  autoload :Git, "gem_contribute/cli/fork_clone_branch"
13
14
  autoload :Submit, "gem_contribute/cli/submit"
15
+ autoload :RateLimitFooter, "gem_contribute/cli/rate_limit_footer"
14
16
  USAGE = <<~USAGE
15
17
  Usage: gem-contribute <command> [options]
16
18
 
17
19
  Commands:
20
+ init One-time interactive setup (sets clone_root).
18
21
  scan [path] Summarize the contributable surface of a Gemfile.lock.
19
22
  Path defaults to ./Gemfile.lock.
20
23
  issues <gem|all> List open "good first issue" issues for a gem (or all gems).
@@ -58,6 +61,7 @@ module GemContribute
58
61
  end
59
62
 
60
63
  COMMANDS = {
64
+ "init" => ->(o, e) { Init.new(stdout: o, stderr: e) },
61
65
  "scan" => ->(o, e) { Scan.new(stdout: o, stderr: e, adapter: github_adapter) },
62
66
  "issues" => ->(o, e) { Issues.new(stdout: o, stderr: e, adapter: github_adapter) },
63
67
  "config" => ->(o, e) { Config.new(stdout: o, stderr: e) },
@@ -8,8 +8,6 @@ module GemContribute
8
8
  # Honors XDG_CONFIG_HOME so tests stay hermetic and unusual layouts work.
9
9
  # Missing or corrupt files are treated as an empty config (no crash).
10
10
  class Config
11
- DEFAULT_CLONE_ROOT = File.expand_path("~/code/oss")
12
-
13
11
  KNOWN_KEYS = %w[clone_root].freeze
14
12
 
15
13
  def initialize(path: self.class.default_path)
@@ -19,7 +17,7 @@ module GemContribute
19
17
 
20
18
  def clone_root
21
19
  raw = @data["clone_root"]
22
- raw ? File.expand_path(raw) : DEFAULT_CLONE_ROOT
20
+ raw ? File.expand_path(raw) : nil
23
21
  end
24
22
 
25
23
  def set(key, value)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GemContribute
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end