gem-contribute 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gem_release.yml +1 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +28 -0
- data/.github/workflows/ci.yml +26 -0
- data/.github/workflows/pr-template-check.yml +100 -0
- data/CHANGELOG.md +41 -0
- data/CLAUDE.md +1 -1
- data/CODE_OF_CONDUCT.md +86 -0
- data/CONTRIBUTING.md +12 -13
- data/README.md +21 -8
- data/docs/OPEN_QUESTIONS.md +167 -0
- data/docs/ROADMAP.md +266 -0
- data/docs/adr/0006-standalone-gem-not-plugin.md +1 -1
- data/docs/adr/0008-rooibos-tui-framework.md +3 -3
- data/docs/adr/0010-charm-ruby-tui-framework.md +84 -0
- data/docs/adr/0011-host-adapter-owns-host-verbs.md +58 -0
- data/docs/adr/0012-output-free-service-objects-three-interface-architecture.md +79 -0
- data/docs/adr/0013-revert-to-rooibos.md +71 -0
- data/docs/adr/0014-ship-bundler-and-rubygems-plugins.md +75 -0
- data/docs/adr/README.md +7 -2
- data/docs/design-interface-layer.md +295 -0
- data/docs/design.md +31 -8
- data/docs/ideas.md +1 -0
- data/docs/index.md +2 -2
- data/docs/prep-plan.md +6 -6
- data/docs/talk/README.md +45 -0
- data/docs/talk/index.html +4165 -0
- data/docs/talk/lightning.md +425 -0
- data/docs/talk/lightning.pdf +0 -0
- data/lib/gem_contribute/cli/auth.rb +22 -44
- data/lib/gem_contribute/cli/config.rb +32 -16
- data/lib/gem_contribute/cli/fix.rb +122 -0
- data/lib/gem_contribute/cli/fork.rb +145 -0
- data/lib/gem_contribute/cli/init.rb +78 -0
- data/lib/gem_contribute/cli/issue_announcer.rb +42 -0
- data/lib/gem_contribute/cli/issues.rb +37 -44
- data/lib/gem_contribute/cli/platform_tools.rb +33 -0
- data/lib/gem_contribute/cli/post_clone_hooks.rb +50 -0
- data/lib/gem_contribute/cli/rate_limit_footer.rb +34 -0
- data/lib/gem_contribute/cli/scan.rb +20 -15
- data/lib/gem_contribute/cli/submit.rb +60 -64
- data/lib/gem_contribute/cli/workflow.rb +63 -0
- data/lib/gem_contribute/cli.rb +11 -14
- data/lib/gem_contribute/config.rb +28 -4
- data/lib/gem_contribute/git.rb +49 -0
- data/lib/gem_contribute/host_adapter.rb +52 -5
- data/lib/gem_contribute/host_adapters/github_adapter.rb +126 -37
- data/lib/gem_contribute/operations/announce.rb +52 -0
- data/lib/gem_contribute/operations/branch.rb +35 -0
- data/lib/gem_contribute/operations/clone.rb +41 -0
- data/lib/gem_contribute/operations/fix_pipeline.rb +70 -0
- data/lib/gem_contribute/operations/fork.rb +35 -0
- data/lib/gem_contribute/output/null.rb +20 -0
- data/lib/gem_contribute/output/standard.rb +71 -0
- data/lib/gem_contribute/version.rb +1 -1
- data/lib/gem_contribute.rb +10 -18
- metadata +120 -3
- data/lib/gem_contribute/cli/fork_clone_branch.rb +0 -197
|
@@ -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
|
|
@@ -10,6 +10,8 @@ module GemContribute
|
|
|
10
10
|
# validate it by hitting /user)
|
|
11
11
|
# logout — drop the cached token for github.com
|
|
12
12
|
class Auth
|
|
13
|
+
include PlatformTools
|
|
14
|
+
|
|
13
15
|
USAGE = <<~USAGE
|
|
14
16
|
Usage: gem-contribute auth <subcommand>
|
|
15
17
|
|
|
@@ -21,11 +23,11 @@ module GemContribute
|
|
|
21
23
|
|
|
22
24
|
DEFAULT_HOST = "github.com"
|
|
23
25
|
|
|
24
|
-
def initialize(stdout: $stdout, stderr: $stderr,
|
|
26
|
+
def initialize(stdout: $stdout, stderr: $stderr, output: nil,
|
|
27
|
+
store: TokenStore.new,
|
|
25
28
|
sleeper: ->(s) { Kernel.sleep(s) },
|
|
26
29
|
browser_opener: nil, clipper: nil)
|
|
27
|
-
@
|
|
28
|
-
@stderr = stderr
|
|
30
|
+
@output = output || Output::Standard.new(out: stdout, err: stderr)
|
|
29
31
|
@store = store
|
|
30
32
|
@sleeper = sleeper
|
|
31
33
|
@browser_opener = browser_opener || method(:default_browser_opener)
|
|
@@ -38,11 +40,11 @@ module GemContribute
|
|
|
38
40
|
when "status" then status
|
|
39
41
|
when "logout" then logout
|
|
40
42
|
when nil, "help", "-h", "--help"
|
|
41
|
-
@
|
|
43
|
+
@output.info(USAGE)
|
|
42
44
|
0
|
|
43
45
|
else
|
|
44
|
-
@
|
|
45
|
-
@
|
|
46
|
+
@output.error("gem-contribute: unknown auth subcommand")
|
|
47
|
+
@output.error(USAGE)
|
|
46
48
|
2
|
|
47
49
|
end
|
|
48
50
|
end
|
|
@@ -55,20 +57,20 @@ module GemContribute
|
|
|
55
57
|
result = poll_loop(device_code)
|
|
56
58
|
persist_or_report(result)
|
|
57
59
|
rescue GemContribute::Auth::AuthError => e
|
|
58
|
-
@
|
|
60
|
+
@output.error("auth login failed: #{e.message}")
|
|
59
61
|
1
|
|
60
62
|
end
|
|
61
63
|
|
|
62
64
|
def prompt_user(device_code)
|
|
63
65
|
copied = @clipper.call(device_code.user_code)
|
|
64
66
|
code_suffix = copied ? " (copied to clipboard)" : ""
|
|
65
|
-
@
|
|
67
|
+
@output.info("Your one-time code#{code_suffix}: #{device_code.user_code}")
|
|
66
68
|
|
|
67
69
|
opened = @browser_opener.call(device_code.verification_uri)
|
|
68
70
|
url_prefix = opened ? "Browser opened to" : "Visit"
|
|
69
|
-
@
|
|
71
|
+
@output.info("#{url_prefix}: #{device_code.verification_uri}")
|
|
70
72
|
|
|
71
|
-
@
|
|
73
|
+
@output.progress("Waiting for you to authorize...")
|
|
72
74
|
end
|
|
73
75
|
|
|
74
76
|
def poll_loop(device_code)
|
|
@@ -92,16 +94,16 @@ module GemContribute
|
|
|
92
94
|
case result.status
|
|
93
95
|
when :ok
|
|
94
96
|
@store.store(DEFAULT_HOST, access_token: result.token, scope: result.scope)
|
|
95
|
-
@
|
|
97
|
+
@output.info("Authenticated. Token saved to #{TokenStore.default_path} (mode 0600).")
|
|
96
98
|
0
|
|
97
99
|
when :expired
|
|
98
|
-
@
|
|
100
|
+
@output.error("Device code expired. Run `gem-contribute auth login` again.")
|
|
99
101
|
1
|
|
100
102
|
when :denied
|
|
101
|
-
@
|
|
103
|
+
@output.error("Authorization denied.")
|
|
102
104
|
1
|
|
103
105
|
else
|
|
104
|
-
@
|
|
106
|
+
@output.error("auth login failed: #{result.error_message}")
|
|
105
107
|
1
|
|
106
108
|
end
|
|
107
109
|
end
|
|
@@ -109,7 +111,7 @@ module GemContribute
|
|
|
109
111
|
def status
|
|
110
112
|
entry = @store.entry_for(DEFAULT_HOST)
|
|
111
113
|
if entry.nil?
|
|
112
|
-
@
|
|
114
|
+
@output.info("Not authenticated. Run `gem-contribute auth login`.")
|
|
113
115
|
return 1
|
|
114
116
|
end
|
|
115
117
|
|
|
@@ -119,46 +121,22 @@ module GemContribute
|
|
|
119
121
|
def verify_and_print(entry)
|
|
120
122
|
adapter = HostAdapters::GitHubAdapter.new(token: entry["access_token"])
|
|
121
123
|
login_name = adapter.viewer_login
|
|
122
|
-
@
|
|
124
|
+
@output.info("Authenticated as @#{login_name} on #{DEFAULT_HOST} (scope: #{entry["scope"] || "unknown"})")
|
|
123
125
|
0
|
|
124
126
|
rescue GemContribute::AuthRequired, GemContribute::AdapterError => e
|
|
125
|
-
@
|
|
126
|
-
@
|
|
127
|
+
@output.error("Token cached for #{DEFAULT_HOST} but verification failed: #{e.message}")
|
|
128
|
+
@output.error("Run `gem-contribute auth login` to refresh.")
|
|
127
129
|
1
|
|
128
130
|
end
|
|
129
131
|
|
|
130
132
|
def logout
|
|
131
133
|
if @store.delete(DEFAULT_HOST)
|
|
132
|
-
@
|
|
134
|
+
@output.info("Logged out of #{DEFAULT_HOST}.")
|
|
133
135
|
else
|
|
134
|
-
@
|
|
136
|
+
@output.info("No cached token for #{DEFAULT_HOST}.")
|
|
135
137
|
end
|
|
136
138
|
0
|
|
137
139
|
end
|
|
138
|
-
|
|
139
|
-
def default_browser_opener(uri)
|
|
140
|
-
cmd = case RbConfig::CONFIG["host_os"]
|
|
141
|
-
when /darwin/ then "open"
|
|
142
|
-
when /linux/ then "xdg-open"
|
|
143
|
-
when /mswin|mingw|cygwin/ then "start"
|
|
144
|
-
end
|
|
145
|
-
cmd && Kernel.system(cmd, uri)
|
|
146
|
-
rescue StandardError
|
|
147
|
-
false
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def default_clipper(text)
|
|
151
|
-
case RbConfig::CONFIG["host_os"]
|
|
152
|
-
when /darwin/
|
|
153
|
-
IO.popen("pbcopy", "w") { |p| p.write(text) }
|
|
154
|
-
true
|
|
155
|
-
when /linux/
|
|
156
|
-
IO.popen(["xclip", "-selection", "clipboard"], "w") { |p| p.write(text) }
|
|
157
|
-
true
|
|
158
|
-
end
|
|
159
|
-
rescue StandardError
|
|
160
|
-
false
|
|
161
|
-
end
|
|
162
140
|
end
|
|
163
141
|
end
|
|
164
142
|
end
|
|
@@ -18,13 +18,21 @@ module GemContribute
|
|
|
18
18
|
list Print all configured values.
|
|
19
19
|
|
|
20
20
|
Keys:
|
|
21
|
-
clone_root Directory where forks are cloned
|
|
22
|
-
|
|
21
|
+
clone_root Directory where forks are cloned. Set with
|
|
22
|
+
`gem-contribute init` (interactive) or
|
|
23
|
+
`gem-contribute config set clone_root <path>`.
|
|
24
|
+
editor Editor command for `fix -e`. Falls back to $EDITOR.
|
|
25
|
+
Example: gem-contribute config set editor code
|
|
26
|
+
ai_tool Shell command for `fix -a` (run in clone dir).
|
|
27
|
+
Example: gem-contribute config set ai_tool "claude ."
|
|
28
|
+
comment_on_fix Whether `fix` posts a "working on this" comment.
|
|
29
|
+
Default: true. Per-repo overrides via
|
|
30
|
+
`comment_on_fix_overrides` in the YAML.
|
|
23
31
|
USAGE
|
|
24
32
|
|
|
25
|
-
def initialize(stdout: $stdout, stderr: $stderr,
|
|
26
|
-
|
|
27
|
-
@
|
|
33
|
+
def initialize(stdout: $stdout, stderr: $stderr, output: nil,
|
|
34
|
+
config: GemContribute::Config.new)
|
|
35
|
+
@output = output || Output::Standard.new(out: stdout, err: stderr)
|
|
28
36
|
@config = config
|
|
29
37
|
end
|
|
30
38
|
|
|
@@ -34,11 +42,11 @@ module GemContribute
|
|
|
34
42
|
when "get" then get(argv)
|
|
35
43
|
when "list" then list
|
|
36
44
|
when nil, "help", "-h", "--help"
|
|
37
|
-
@
|
|
45
|
+
@output.info(USAGE)
|
|
38
46
|
0
|
|
39
47
|
else
|
|
40
|
-
@
|
|
41
|
-
@
|
|
48
|
+
@output.error("gem-contribute: unknown config subcommand")
|
|
49
|
+
@output.error(USAGE)
|
|
42
50
|
2
|
|
43
51
|
end
|
|
44
52
|
end
|
|
@@ -49,37 +57,45 @@ module GemContribute
|
|
|
49
57
|
key = argv.shift
|
|
50
58
|
value = argv.shift
|
|
51
59
|
if key.nil? || value.nil?
|
|
52
|
-
@
|
|
60
|
+
@output.error("Usage: gem-contribute config set <key> <value>")
|
|
53
61
|
return 2
|
|
54
62
|
end
|
|
55
63
|
|
|
56
64
|
@config.set(key, value)
|
|
57
|
-
@
|
|
65
|
+
@output.info("#{key} = #{value}")
|
|
58
66
|
0
|
|
59
67
|
rescue ArgumentError => e
|
|
60
|
-
@
|
|
68
|
+
@output.error(e.message)
|
|
61
69
|
1
|
|
62
70
|
end
|
|
63
71
|
|
|
64
72
|
def get(argv)
|
|
65
73
|
key = argv.shift
|
|
66
74
|
if key.nil?
|
|
67
|
-
@
|
|
75
|
+
@output.error("Usage: gem-contribute config get <key>")
|
|
68
76
|
return 2
|
|
69
77
|
end
|
|
70
78
|
|
|
71
79
|
unless GemContribute::Config::KNOWN_KEYS.include?(key)
|
|
72
|
-
@
|
|
80
|
+
@output.error("unknown config key #{key.inspect}")
|
|
73
81
|
return 1
|
|
74
82
|
end
|
|
75
83
|
|
|
76
|
-
@
|
|
84
|
+
@output.info(@config.to_h.fetch(key, "(not set; run `gem-contribute init`)"))
|
|
77
85
|
0
|
|
78
86
|
end
|
|
79
87
|
|
|
80
88
|
def list
|
|
81
|
-
@
|
|
82
|
-
@
|
|
89
|
+
@output.info("Configuration (#{GemContribute::Config.default_path}):")
|
|
90
|
+
@output.info(" clone_root = #{@config.clone_root || "(not set; run `gem-contribute init`)"}")
|
|
91
|
+
@output.info(" editor = #{@config.editor || "(not set)"}")
|
|
92
|
+
@output.info(" ai_tool = #{@config.ai_tool || "(not set)"}")
|
|
93
|
+
@output.info(" comment_on_fix = #{@config.comment_on_fix?}")
|
|
94
|
+
overrides = @config.to_h["comment_on_fix_overrides"]
|
|
95
|
+
if overrides.is_a?(Hash) && !overrides.empty?
|
|
96
|
+
@output.info(" comment_on_fix_overrides:")
|
|
97
|
+
overrides.each { |repo, val| @output.info(" #{repo}: #{val}") }
|
|
98
|
+
end
|
|
83
99
|
0
|
|
84
100
|
end
|
|
85
101
|
end
|