gem-contribute 0.1.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 +7 -0
- data/.github/ISSUE_TEMPLATE/workshop-issue.md +29 -0
- data/.github/workflows/auto-merge-kicked-tires.yml +88 -0
- data/CHANGELOG.md +24 -0
- data/CLAUDE.md +47 -0
- data/CONTRIBUTING.md +46 -0
- data/KICKED_THE_TIRES.yml +22 -0
- data/LICENSE +21 -0
- data/MAINTAINER.md +92 -0
- data/README.md +89 -0
- data/Rakefile +10 -0
- data/docs/_config.yml +30 -0
- data/docs/adr/0001-just-in-time-auth.md +44 -0
- data/docs/adr/0002-bundler-lockfile-parser.md +35 -0
- data/docs/adr/0003-issue-tracker-preference.md +33 -0
- data/docs/adr/0004-device-flow-auth.md +36 -0
- data/docs/adr/0005-render-labels-verbatim.md +46 -0
- data/docs/adr/0006-standalone-gem-not-plugin.md +31 -0
- data/docs/adr/0007-display-contributing-verbatim.md +39 -0
- data/docs/adr/0008-rooibos-tui-framework.md +62 -0
- data/docs/adr/0009-top-level-namespace.md +37 -0
- data/docs/adr/README.md +21 -0
- data/docs/claude-code-prompt.md +40 -0
- data/docs/design.md +234 -0
- data/docs/index.md +102 -0
- data/docs/prep-plan.md +165 -0
- data/docs/workshop.md +60 -0
- data/exe/gem-contribute +7 -0
- data/lib/gem_contribute/auth.rb +161 -0
- data/lib/gem_contribute/cache.rb +98 -0
- data/lib/gem_contribute/cli/auth.rb +164 -0
- data/lib/gem_contribute/cli/config.rb +87 -0
- data/lib/gem_contribute/cli/fork_clone_branch.rb +197 -0
- data/lib/gem_contribute/cli/issues.rb +123 -0
- data/lib/gem_contribute/cli/scan.rb +117 -0
- data/lib/gem_contribute/cli/submit.rb +155 -0
- data/lib/gem_contribute/cli.rb +104 -0
- data/lib/gem_contribute/config.rb +60 -0
- data/lib/gem_contribute/errors.rb +32 -0
- data/lib/gem_contribute/host_adapter.rb +40 -0
- data/lib/gem_contribute/host_adapters/github_adapter.rb +215 -0
- data/lib/gem_contribute/locked_gem.rb +26 -0
- data/lib/gem_contribute/lockfile_parser.rb +61 -0
- data/lib/gem_contribute/project.rb +21 -0
- data/lib/gem_contribute/resolver.rb +131 -0
- data/lib/gem_contribute/token_store.rb +86 -0
- data/lib/gem_contribute/version.rb +5 -0
- data/lib/gem_contribute.rb +32 -0
- data/script/lint-kicked-tires.rb +76 -0
- data/sig/gem_contribute.rbs +3 -0
- metadata +114 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# ADR 0004: OAuth Device Flow, not Personal Access Tokens
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted
|
|
4
|
+
**Date:** 2026-04-27
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
To fork repos and clone forks on a user's behalf, `gem-contribute` needs an authenticated GitHub session. Two practical options:
|
|
9
|
+
|
|
10
|
+
1. **Personal Access Token (PAT).** User generates a token in GitHub settings, pastes it into the tool.
|
|
11
|
+
2. **OAuth Device Authorization Grant ("device flow").** Tool displays a code; user opens browser, signs in, enters code; tool polls for the token. Same UX as `gh auth login`.
|
|
12
|
+
|
|
13
|
+
## Decision
|
|
14
|
+
|
|
15
|
+
Device flow.
|
|
16
|
+
|
|
17
|
+
## Reasoning
|
|
18
|
+
|
|
19
|
+
UX. The PAT flow is genuinely awful: navigate to settings, click through several screens, choose scopes you don't fully understand, name the token, copy it within the one-time-display window, paste it into the tool, hope you didn't fat-finger it. Half the people in the workshop room will lose three minutes to this and one will lose ten.
|
|
20
|
+
|
|
21
|
+
Device flow is approximately: type `gem-contribute`, click a button in your already-open browser, done. About 30 seconds, no copy-paste, no leaked tokens in shell history.
|
|
22
|
+
|
|
23
|
+
Critically for an open-source CLI: device flow needs only a `client_id`, no client secret. We can ship the client ID as a public constant in the source code. There is no secret to protect. This is by design — GitHub's docs explicitly support this pattern.
|
|
24
|
+
|
|
25
|
+
## Alternatives considered
|
|
26
|
+
|
|
27
|
+
- **PAT only.** Rejected for UX reasons above.
|
|
28
|
+
- **Both, with PAT as fallback.** Reasonable; deferred to v0.2 if anyone asks. The ground-truth use case (corporate networks where the device-flow polling fails for some reason) is real but rare.
|
|
29
|
+
- **GitHub App instead of OAuth App.** GitHub Apps are more powerful and more correct in the long run, but they require token refresh logic and a higher prep burden. Defer until there's a reason to switch.
|
|
30
|
+
|
|
31
|
+
## Consequences
|
|
32
|
+
|
|
33
|
+
- The maintainer (initially: Chris) registers an OAuth App on a personal GitHub account and copies the client ID into the source. When the tool is donated to a more permanent home, the OAuth App migrates with it.
|
|
34
|
+
- Rate limits: 50 `user_code` submissions per hour across all users of this client ID. With workshop-scale usage (~12 attendees authing once each), this is fine. If the tool ever gets popular enough to brush this limit, we register additional OAuth Apps or switch to a GitHub App.
|
|
35
|
+
- We must implement: device code request, polling with `slow_down` backoff, token storage at `~/.config/gem-contribute/auth.json` (mode 0600), and graceful handling of the 15-minute device-code expiry.
|
|
36
|
+
- Scope is `public_repo` only at v1. Adding `repo` (for private repos) would be a future ADR.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# ADR 0005: Render labels verbatim
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted
|
|
4
|
+
**Date:** 2026-04-27
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
GitHub label conventions vary across projects. Common variants meaning roughly "this is approachable for new contributors":
|
|
9
|
+
|
|
10
|
+
- `good first issue`
|
|
11
|
+
- `good-first-issue`
|
|
12
|
+
- `Good First Issue`
|
|
13
|
+
- `beginner`
|
|
14
|
+
- `easy`
|
|
15
|
+
- `help wanted`
|
|
16
|
+
- `up-for-grabs`
|
|
17
|
+
- `low-hanging fruit`
|
|
18
|
+
- (none — the maintainer doesn't tag at all)
|
|
19
|
+
|
|
20
|
+
We could normalize these to a canonical set ("Beginner-friendly", "Help wanted", etc.) for cleaner UI. We could also leave them exactly as the maintainer wrote them.
|
|
21
|
+
|
|
22
|
+
## Decision
|
|
23
|
+
|
|
24
|
+
Render labels exactly as the maintainer wrote them, with the colors GitHub returns. Allow the user to specify a `preferred_labels` list in config that controls highlighting and sort order, but don't rewrite anything.
|
|
25
|
+
|
|
26
|
+
## Reasoning
|
|
27
|
+
|
|
28
|
+
Two reasons, one technical and one social.
|
|
29
|
+
|
|
30
|
+
**Technical:** Normalization is a heuristic. Heuristics are wrong sometimes. When the heuristic gets it wrong — say, a project uses `easy` to mean "easy to fix once you understand the architecture" rather than "easy for a beginner" — normalization loses information the maintainer encoded deliberately. The user is better served by the raw label and the exercise of reading it in context.
|
|
31
|
+
|
|
32
|
+
**Social:** The act of reading a project's labels *is* contributor onboarding. It's how you start to understand a project's voice, its triage workflow, its expectations. A tool that smooths that over removes a learning surface. We are not in the business of removing learning surfaces from people who are explicitly here to learn open-source contribution.
|
|
33
|
+
|
|
34
|
+
The `preferred_labels` config is the escape valve for users who want their own ranking. It's user-controlled, not algorithmic. Different.
|
|
35
|
+
|
|
36
|
+
## Alternatives considered
|
|
37
|
+
|
|
38
|
+
- **Normalize to a fixed taxonomy.** Loses information; opinionated in a way the tool shouldn't be. Rejected.
|
|
39
|
+
- **Show normalized labels alongside raw ones.** Visual clutter. The thing we're optimizing for (fast scanning of issue lists) gets worse, not better. Rejected.
|
|
40
|
+
- **Let the user define normalizations in config.** This is approximately what `preferred_labels` does without claiming to normalize. Accepted in that form.
|
|
41
|
+
|
|
42
|
+
## Consequences
|
|
43
|
+
|
|
44
|
+
- Issue list rendering must preserve label colors from the GitHub API.
|
|
45
|
+
- `preferred_labels` matches case-insensitively against the raw label text. Hyphens and spaces are treated as equivalent. That's the only normalization we do, and it's only for the user's own preference matching.
|
|
46
|
+
- Users who want a different taxonomy can edit their config. We don't ship one.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# ADR 0006: Standalone gem, not a Bundler plugin
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted
|
|
4
|
+
**Date:** 2026-04-27
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
Bundler supports plugins that extend `bundle` with new subcommands. A natural-feeling distribution would be `bundle contribute`, mirroring `bundle fund`. Alternatively, we ship as a standalone gem invoked as `gem-contribute`.
|
|
9
|
+
|
|
10
|
+
## Decision
|
|
11
|
+
|
|
12
|
+
Standalone gem at v1. Bundler plugin path is not foreclosed; it just isn't v1's problem.
|
|
13
|
+
|
|
14
|
+
## Reasoning
|
|
15
|
+
|
|
16
|
+
Bundler plugin authoring has its own learning curve, its own API surface, and its own debugging story. None of that is the part of this project we want attendees of the Blue Ridge Ruby workshop to learn. They're here to learn Ratatui and OAuth and GitHub's API. The Bundler plugin packaging concerns would actively distract.
|
|
17
|
+
|
|
18
|
+
A standalone gem also keeps the dev loop shorter: clone, `bundle install`, `bin/gem-contribute`. Plugin development requires installing into Bundler's plugin directory and reasoning about how Bundler isolates plugin gems.
|
|
19
|
+
|
|
20
|
+
The user-facing UX difference is small. `bundle contribute` is two characters shorter than `gem-contribute` and feels more native. That's not zero, but it's not v1's priority either.
|
|
21
|
+
|
|
22
|
+
## Alternatives considered
|
|
23
|
+
|
|
24
|
+
- **Plugin from day one.** Rejected: scope creep for the workshop; harder to maintain; harder for new contributors to dive into.
|
|
25
|
+
- **Both.** Rejected: more API surface to keep aligned, double the support burden.
|
|
26
|
+
|
|
27
|
+
## Consequences
|
|
28
|
+
|
|
29
|
+
- The CLI binary is `gem-contribute`, not `bundle contribute`.
|
|
30
|
+
- Users who want the `bundle contribute` UX can write a one-line shell alias.
|
|
31
|
+
- A future ADR can revisit this if the tool sees real adoption and the plugin UX becomes the bottleneck.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# ADR 0007: Show CONTRIBUTING; don't parse it
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted
|
|
4
|
+
**Date:** 2026-04-27
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
When a user is about to fork-and-branch on an issue, it would be useful to surface the project's contribution guidelines. CONTRIBUTING.md is the canonical location, with `.github/CONTRIBUTING.md` and `docs/CONTRIBUTING.md` as common alternates. GitHub's community profile API (`/repos/:owner/:repo/community/profile`) returns the path of whichever exists.
|
|
9
|
+
|
|
10
|
+
Two ways to use that file:
|
|
11
|
+
|
|
12
|
+
1. **Display it.** Show the markdown in a pane. User reads it.
|
|
13
|
+
2. **Parse it.** Programmatically extract things like "this project uses these labels," "PRs require DCO sign-off," "tests are required," etc.
|
|
14
|
+
|
|
15
|
+
## Decision
|
|
16
|
+
|
|
17
|
+
Display, don't parse.
|
|
18
|
+
|
|
19
|
+
## Reasoning
|
|
20
|
+
|
|
21
|
+
Parsing CONTRIBUTING.md across thousands of projects is heuristic work. The files are written in prose by humans for humans, with no schema, no consistent terminology, and no obligation to be parseable. Any extraction layer we ship will be wrong some of the time, in subtle ways, on the projects that matter most (the ones with non-standard but important guidelines).
|
|
22
|
+
|
|
23
|
+
The cost of being wrong here is high. A user who relies on a misparsed CONTRIBUTING and opens a PR that violates an unwritten convention is in a worse position than a user who reads the file themselves and notices the convention.
|
|
24
|
+
|
|
25
|
+
There's also a softer reason: reading a project's CONTRIBUTING is part of the contribution itself. It's where you learn the project's voice and norms. Hiding that behind extracted bullet points makes the user a worse contributor over time.
|
|
26
|
+
|
|
27
|
+
The pragmatic version of "display, don't parse" is good enough: render the markdown nicely (headings, lists, code blocks, links), let the user scroll through it, surface a "you haven't read this yet" indicator on the issue detail screen if they try to fork before opening CONTRIBUTING.
|
|
28
|
+
|
|
29
|
+
## Alternatives considered
|
|
30
|
+
|
|
31
|
+
- **Parse for known signals.** Look for backticked label names, URL patterns, common phrases like "sign your commits." Possibly v0.3+; not v1. The risk is that we introduce the parsing infrastructure and then everyone wants to extend it, and we end up with a half-built NLP system in a TUI tool.
|
|
32
|
+
- **AI-summarize the CONTRIBUTING.** Out of scope, out of character for this project, and an additional dependency we don't want.
|
|
33
|
+
- **Don't surface CONTRIBUTING at all.** Surfacing it is one of the tool's quietly-best features. Rejected.
|
|
34
|
+
|
|
35
|
+
## Consequences
|
|
36
|
+
|
|
37
|
+
- We need a markdown renderer that works in a Ratatui pane. There are a few; pick the smallest one that handles headings, lists, code blocks, and links.
|
|
38
|
+
- The "have you read CONTRIBUTING" indicator is local UI state. We track whether the user has opened the CONTRIBUTING pane for the current project in this session. We don't persist this — re-prompting on a fresh run is fine.
|
|
39
|
+
- If someone wants parsing, they can build it as a separate gem that consumes our output.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# ADR 0008: Use Rooibos for the TUI layer
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted
|
|
4
|
+
**Date:** 2026-04-27
|
|
5
|
+
**Supersedes parts of:** the original "TUI built directly on `ratatui_ruby`" approach implied by ADR-0001 and the `docs/design.md` v1.
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
`gem-contribute` is a TUI that needs to:
|
|
10
|
+
|
|
11
|
+
1. Make multiple HTTP calls (RubyGems API, GitHub API, GitHub OAuth device flow polling) without freezing the UI.
|
|
12
|
+
2. Shell out to `git` for fork-clone-branch, also without freezing the UI.
|
|
13
|
+
3. Compose four primary views (project list → issues → issue detail → CONTRIBUTING) with the auth-prompt flow able to interrupt any of them.
|
|
14
|
+
4. Be testable enough that the gem can be maintained past the conference without breaking on every PR.
|
|
15
|
+
|
|
16
|
+
[`ratatui_ruby`](https://www.ratatui-ruby.dev/) provides the rendering layer, but leaves state management, threading, message dispatch, and testing as exercises for the consumer. [`rooibos`](https://www.rooibos.run/) is a higher-level framework by the same maintainer (Kerrick Long) that layers Model-View-Update on top of `ratatui_ruby`, with async Commands for off-thread work and built-in snapshot testing.
|
|
17
|
+
|
|
18
|
+
## Decision
|
|
19
|
+
|
|
20
|
+
Use Rooibos as the TUI framework. `ratatui_ruby` remains a transitive dependency for rendering and widgets, but the application's state, message handling, and async work are expressed in Rooibos terms.
|
|
21
|
+
|
|
22
|
+
## Reasoning
|
|
23
|
+
|
|
24
|
+
**The async command pattern is the right abstraction for our problem.** Fork-clone-branch can take 30+ seconds against a large repo. GitHub API calls are routinely 200-500ms. Device-flow polling runs every 5 seconds for up to 15 minutes. Doing any of these on the main thread freezes the UI; doing them ourselves means hand-rolling thread management, message queues, and cancellation. Rooibos provides `Command.system`, `Command.http`, `Command.wait`, and `Command.cancel` as first-class primitives that run off-thread and deliver results back as messages. This is exactly the surface we need.
|
|
25
|
+
|
|
26
|
+
**Testing is dramatically better.** The original design doc said "no TUI tests at v1, the cost-benefit isn't there." With Rooibos, `Update` is a pure function `(message, model) → model | [model, command]`. Pure functions test trivially, no terminal, no setup, no mocking. View tests use a headless terminal with style assertions. System tests inject events and snapshot results. The pre-conference test commitment goes from "parsers and resolvers only" to "the entire state machine, including the auth flow." This isn't a stretch goal — it's free with the framework.
|
|
27
|
+
|
|
28
|
+
**The fractal architecture maps to our four-view structure.** Rooibos's Router DSL composes parent fragments out of child fragments. Each view (project list, issue list, issue detail, CONTRIBUTING viewer) becomes a fragment with its own `Model`, `View`, `Update`, and `Init`. The parent dispatches messages to children based on routing rules. This is a structure we'd have to invent and document if we built directly on `ratatui_ruby`; we get it for free.
|
|
29
|
+
|
|
30
|
+
**The auth flow becomes legible.** With imperative Ratatui, JIT auth requires interrupting the current screen, blocking on a sub-flow, and resuming. With MVU, an `:auth_required` message triggers a state transition; the device-flow polling is a sequence of Commands; the original action retries via another message after success. The whole thing is a state machine, expressed in code as a state machine, testable as a state machine. See ADR-0001 for what this changes.
|
|
31
|
+
|
|
32
|
+
**Same maintainer as `ratatui_ruby`.** Reduces the chance of cross-library impedance mismatch. Rooibos is the maintainer's opinionated answer to "how should you actually build with this rendering layer."
|
|
33
|
+
|
|
34
|
+
## Alternatives considered
|
|
35
|
+
|
|
36
|
+
- **Plain `ratatui_ruby` with our own state and threading.** What the design doc originally implied. Rejected: more code to write and maintain, worse testing story, and we'd be reinventing primitives that Rooibos already provides better. The savings from "fewer dependencies" are dwarfed by the cost of building this layer ourselves.
|
|
37
|
+
|
|
38
|
+
- **Kit.** Also by Kerrick, OOP component-based, tracked at <https://sr.ht/~kerrick/ratatui_ruby/#chapter-3-the-object-path--kit>. Reasonable for component-heavy UIs with stateful widgets. Rejected for this project: our domain is event-driven and async-heavy (HTTP, system calls, polling), which matches MVU's strengths, and pure-function `Update` is the testing story we want.
|
|
39
|
+
|
|
40
|
+
- **Wait for Rooibos 1.0.** Rooibos is currently 0.7 with "APIs may change before 1.0." Waiting is the conservative choice. Rejected: the 1.0 timeline is unknown, and the architectural fit is too good to defer. We pin to a specific version and adapt to changes when they come.
|
|
41
|
+
|
|
42
|
+
## Consequences
|
|
43
|
+
|
|
44
|
+
**On the design doc:** the "Modules" section needs revision. Views become Rooibos fragments, not bare classes. The `Worker` module disappears — fork-clone-branch is a sequence of Commands emitted from `Update`. The architecture diagram becomes MVU-shaped. Testing strategy shifts from "test the boundaries, skip the TUI" to "test the Update functions everywhere."
|
|
45
|
+
|
|
46
|
+
**On dependencies:** add `rooibos` to the gemspec. Pin to `~> 0.7.0` for v0.1 (allows patch updates within 0.7, blocks 0.8+ until we audit). Bump deliberately, with an ADR if the bump requires meaningful changes.
|
|
47
|
+
|
|
48
|
+
**On the workshop:** attendees learn MVU, not just `ratatui_ruby` widgets. This is a real cost — the lambda-as-constant style (`Init = ->`, `View = ->`) is unfamiliar to most Rails developers. Mitigation: the workshop README explicitly frames Rooibos as "the framework," explains MVU in two paragraphs, and points at the "Coming From Rails" guide on rooibos.run before the workshop. Attendees who finish a Rooibos workshop end up with an actually-transferable mental model (MVU shows up in Elm, Redux, Bubble Tea, and increasingly elsewhere).
|
|
49
|
+
|
|
50
|
+
**On Ractor:** Rooibos uses `Ractor.make_shareable` for thread-safe state. Most Ruby developers have read about Ractors but not used them. The pattern is encapsulated in `Init` and `Update.with(...)`; attendees don't need a deep Ractor mental model to write fragments. Worth a sentence in the workshop preamble, not more.
|
|
51
|
+
|
|
52
|
+
**On the maintainer relationship:** Kerrick Long maintains both `ratatui_ruby` and Rooibos. Reaching out before the workshop to mention "we're building a workshop project on Rooibos for Blue Ridge Ruby" is good practice — early flag of API changes, possible feedback, possible amplification.
|
|
53
|
+
|
|
54
|
+
## What this *doesn't* change
|
|
55
|
+
|
|
56
|
+
- Just-in-time auth (ADR-0001). Implementation cleaner; decision unchanged.
|
|
57
|
+
- Bundler's lockfile parser (ADR-0002). Outside the TUI layer entirely.
|
|
58
|
+
- Issue tracker URI preference (ADR-0003). Outside the TUI layer.
|
|
59
|
+
- Device flow auth (ADR-0004). The flow becomes a sequence of `Command.http` calls in `Update`, but the protocol decision is unchanged.
|
|
60
|
+
- Render labels verbatim (ADR-0005). Display concern; the framework rendering them doesn't matter.
|
|
61
|
+
- Standalone gem vs Bundler plugin (ADR-0006). Packaging concern; orthogonal.
|
|
62
|
+
- Display CONTRIBUTING (ADR-0007). Same.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# ADR 0009: Top-level namespace is `GemContribute`, not `Gem::Contribute`
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted
|
|
4
|
+
**Date:** 2026-04-27
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
Running `bundle gem gem-contribute` (and, by extension, `rooibos new .` on a hyphenated gem name) produces a skeleton that nests the project under `Gem::Contribute`. The first commit of this repo did exactly that: `lib/gem/contribute.rb`, `module Gem; module Contribute`.
|
|
9
|
+
|
|
10
|
+
`Gem` is the namespace of Ruby's stdlib package-management library (`Gem::Specification`, `Gem::Version`, `Gem::Requirement`, `Gem::Dependency`, etc., plus the global `Gem` module method on every spec file we ship). Reopening it for our own application code mixes two unrelated namespaces.
|
|
11
|
+
|
|
12
|
+
The design doc and ADRs (0001–0008) consistently describe the modules with bare names — `LockfileParser`, `Resolver`, `HostAdapter`, `GitHubAdapter`, `Auth` — never `Gem::Contribute::LockfileParser`. The prose treats them as siblings, not children of `Gem`.
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
Top-level namespace is `GemContribute`. Files live under `lib/gem_contribute/`, with a primary `lib/gem_contribute.rb` entry point. The gem name on RubyGems stays `gem-contribute` (the binary stays `gem-contribute`); only the in-code constant changes.
|
|
17
|
+
|
|
18
|
+
## Reasoning
|
|
19
|
+
|
|
20
|
+
**Avoids stdlib collisions.** Inside `module Gem::Contribute`, every reference to `Gem` resolves to *our* reopened module first, not to stdlib. This is fine until it isn't — the moment we reach for `Gem::Specification` or `Gem::Version` (entirely plausible in a tool that talks to RubyGems) we get either a confusing constant lookup or a Rubocop `Lint/ConstantDefinitionInBlock` style warning. Cheaper to never start.
|
|
21
|
+
|
|
22
|
+
**Matches the design doc's vocabulary.** ADRs and `docs/design.md` describe modules as if they live in their own namespace. Naming the namespace `GemContribute` makes the code read the way the docs read.
|
|
23
|
+
|
|
24
|
+
**Internal `LockedGem` struct.** The design doc's prose uses "Gem" for the parsed lockfile entry. To keep that meaning without collision-prone identifiers (`GemContribute::Gem` would shadow stdlib's `::Gem` inside the module body), the value object is named `GemContribute::LockedGem` and the user-facing prose still calls it "a gem from the lockfile."
|
|
25
|
+
|
|
26
|
+
## Alternatives considered
|
|
27
|
+
|
|
28
|
+
- **Keep `Gem::Contribute`.** What `bundle gem` produces by default. Rejected for the reasons above. The default is a default; defaults are sometimes wrong.
|
|
29
|
+
- **Top-level `Contribute` module.** Short and clean, but `Contribute` as a top-level constant is presumptuous — too many other tools could plausibly use it.
|
|
30
|
+
- **`BlueRidge::GemContribute` or `Workshop::GemContribute`.** Workshop-flavored, but the gem outlives the workshop. Rejected.
|
|
31
|
+
|
|
32
|
+
## Consequences
|
|
33
|
+
|
|
34
|
+
- All Ruby files use `GemContribute::Whatever`. RBS signatures match.
|
|
35
|
+
- `lib/` layout is `lib/gem_contribute.rb` + `lib/gem_contribute/*.rb`, not `lib/gem/contribute.rb`.
|
|
36
|
+
- The gemspec's `require "gem_contribute/version"` replaces `require "gem/contribute/version"`.
|
|
37
|
+
- Existing skeleton files from `rooibos new .` (`lib/gem/`, `sig/gem/`, `test/gem/`) are removed; this is a pre-stage-1 reset, not a refactor mid-stream.
|
data/docs/adr/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Architecture Decision Records
|
|
2
|
+
|
|
3
|
+
This directory contains short, dated records of meaningful design decisions. Each ADR captures one decision, the context it was made in, the alternatives considered, and the consequences accepted. The goal is auditable reasoning, not exhaustive documentation.
|
|
4
|
+
|
|
5
|
+
Format: [Michael Nygard's template](https://github.com/joelparkerhenderson/architecture-decision-record/blob/main/locales/en/templates/decision-record-template-by-michael-nygard/index.md), kept short.
|
|
6
|
+
|
|
7
|
+
## Index
|
|
8
|
+
|
|
9
|
+
- [0001 — Just-in-time auth](0001-just-in-time-auth.md)
|
|
10
|
+
- [0002 — Use Bundler's lockfile parser](0002-bundler-lockfile-parser.md)
|
|
11
|
+
- [0003 — Prefer `bug_tracker_uri` over `source_code_uri`](0003-issue-tracker-preference.md)
|
|
12
|
+
- [0004 — Use OAuth Device Flow, not PATs](0004-device-flow-auth.md)
|
|
13
|
+
- [0005 — Render labels verbatim](0005-render-labels-verbatim.md)
|
|
14
|
+
- [0006 — Ship as a standalone gem, not a Bundler plugin](0006-standalone-gem-not-plugin.md)
|
|
15
|
+
- [0007 — Show CONTRIBUTING; don't parse it](0007-display-contributing-verbatim.md)
|
|
16
|
+
- [0008 — Use Rooibos for the TUI layer](0008-rooibos-tui-framework.md)
|
|
17
|
+
- [0009 — Top-level namespace is `GemContribute`](0009-top-level-namespace.md)
|
|
18
|
+
|
|
19
|
+
## When to add an ADR
|
|
20
|
+
|
|
21
|
+
Add one when a decision is non-obvious *and* would be expensive to reverse. Don't add one for "we used Minitest." Do add one when "we picked X over Y for non-obvious reasons and someone six months from now will wonder why."
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Initial prompt for Claude Code
|
|
2
|
+
|
|
3
|
+
Paste the following as your first message in Claude Code (in the `gem-contribute` directory).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
We're building `gem-contribute` together for a workshop at Blue Ridge Ruby on April 30 – May 1, 2026.
|
|
8
|
+
|
|
9
|
+
Before doing anything, read these in order:
|
|
10
|
+
|
|
11
|
+
1. `CLAUDE.md` — the working agreement
|
|
12
|
+
2. `README.md` — what the project is
|
|
13
|
+
3. `docs/design.md` — the architecture
|
|
14
|
+
4. `docs/adr/` — every ADR; they constrain implementation choices
|
|
15
|
+
5. `docs/prep-plan.md` — the staged plan I want you to execute
|
|
16
|
+
|
|
17
|
+
Then check out the current state of the repo. Right now there's no Ruby code yet — just docs. The very first thing you do is generate the gem skeleton (gemspec, Gemfile, lib/, bin/, spec/) following standard Bundler conventions for a CLI gem with a native-extension dependency. Use the gem name, version, and license from the README. Don't add any runtime dependencies that aren't justified by the design doc; we'll add `rooibos` and `ratatui_ruby` in Stage 3.
|
|
18
|
+
|
|
19
|
+
Then start Stage 1 from `docs/prep-plan.md`.
|
|
20
|
+
|
|
21
|
+
Working rules for our collaboration:
|
|
22
|
+
|
|
23
|
+
- **Stop at every stage boundary** in the prep plan and tell me what you've done. Don't barrel into the next stage. I want to demo and review.
|
|
24
|
+
- **Commit at meaningful checkpoints**, not all at once at the end. Conventional commits (`feat:`, `fix:`, `docs:`, `test:`, `refactor:`).
|
|
25
|
+
- **Push back on me** if a request contradicts an ADR. Reference the ADR by number. If we change our minds, we update the ADR before writing the conflicting code.
|
|
26
|
+
- **When you hit a real architecture question**, surface it instead of picking. The ADR-0008 / Rooibos call is the kind of thing that should have come back to me as a question, not a fait accompli.
|
|
27
|
+
- **Don't write workshop issues as actual GitHub issues**, just as markdown files per Stage 4. I'll create them on GitHub myself when the repo is public.
|
|
28
|
+
- **Don't register the OAuth App for me.** When Stage 2 needs the client ID, generate a `MAINTAINER.md` with step-by-step instructions for me to do it manually, then pause and wait for me to paste the client ID back.
|
|
29
|
+
- **Don't open the demo PR for me.** When Stage 2 says "use the tool to open one real PR," stop there and have me do it. Watching me use my own tool is the test.
|
|
30
|
+
|
|
31
|
+
Start by reading the docs and confirming the plan. Then go.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Notes on using this prompt
|
|
36
|
+
|
|
37
|
+
- If Claude Code asks clarifying questions before reading the docs, tell it to read first and then ask.
|
|
38
|
+
- If it suggests architecture changes early, fine — but require an ADR update *before* the code change, not after.
|
|
39
|
+
- The "stop at every stage boundary" rule is the most important one. Without it, agentic coding sessions tend to overshoot. Reinforce it if needed.
|
|
40
|
+
- The 12–20 hour estimate in `prep-plan.md` is honest, not pessimistic. Plan accordingly.
|
data/docs/design.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Design
|
|
2
|
+
|
|
3
|
+
This document describes the architecture of `gem-contribute` — what the pieces are, how they fit together, and why. It's the document you'd read before making a non-trivial change. For the *reasoning* behind specific choices, see [`adr/`](adr/).
|
|
4
|
+
|
|
5
|
+
## Goal
|
|
6
|
+
|
|
7
|
+
Help a Ruby developer contribute to the open-source projects they already depend on, with the lowest possible friction between "I noticed an issue" and "I have a working branch."
|
|
8
|
+
|
|
9
|
+
That's the only goal. It's worth restating because it disqualifies a lot of adjacent ideas: this is not a general issue browser, not a PR review tool, not a "discover new gems" tool, not a project management tool. The lockfile is the scope.
|
|
10
|
+
|
|
11
|
+
## The two halves
|
|
12
|
+
|
|
13
|
+
`gem-contribute` has a clean split between the **data layer** (parsers, resolvers, host adapters, auth) and the **TUI layer** (a Rooibos MVU app). The data layer knows nothing about the UI. The TUI layer talks to the data layer only through Commands and messages.
|
|
14
|
+
|
|
15
|
+
This split is what makes the offline mode, the test suite, and the future "GitLab adapter weekend project" tractable. Don't violate it.
|
|
16
|
+
|
|
17
|
+
## Data layer
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
Gemfile.lock
|
|
21
|
+
│
|
|
22
|
+
▼
|
|
23
|
+
┌─────────────────┐
|
|
24
|
+
│ LockfileParser │ no network
|
|
25
|
+
└────────┬────────┘
|
|
26
|
+
▼
|
|
27
|
+
[Gem, Gem, …]
|
|
28
|
+
│
|
|
29
|
+
▼
|
|
30
|
+
┌─────────────────┐
|
|
31
|
+
│ Resolver │ anonymous RubyGems API
|
|
32
|
+
└────────┬────────┘
|
|
33
|
+
▼
|
|
34
|
+
[Project(host, owner, repo), …]
|
|
35
|
+
│
|
|
36
|
+
▼
|
|
37
|
+
┌─────────────────┐
|
|
38
|
+
│ HostAdapter │ auth checked just-in-time,
|
|
39
|
+
│ (per-host) │ per-host
|
|
40
|
+
└─────────────────┘
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Each stage produces values the next stage consumes. No reverse calls.
|
|
44
|
+
|
|
45
|
+
### `LockfileParser`
|
|
46
|
+
|
|
47
|
+
Input: a path to `Gemfile.lock`.
|
|
48
|
+
Output: a list of `Gem` structs (`name`, `version`, `source` — where `source` is `:rubygems`, `:git`, `:path`, etc.).
|
|
49
|
+
|
|
50
|
+
Pure parsing, no network. Wraps `Bundler::LockfileParser`. See [ADR-0002](adr/0002-bundler-lockfile-parser.md).
|
|
51
|
+
|
|
52
|
+
### `Resolver`
|
|
53
|
+
|
|
54
|
+
Input: a `Gem`.
|
|
55
|
+
Output: a `Project` (`host`, `owner`, `repo`, `metadata`) or `nil` if unresolvable.
|
|
56
|
+
|
|
57
|
+
Hits the RubyGems v1 API anonymously. Prefers `bug_tracker_uri` over `source_code_uri` — see [ADR-0003](adr/0003-issue-tracker-preference.md). Caches under `~/.cache/gem-contribute/`.
|
|
58
|
+
|
|
59
|
+
The `host` is parsed from the URL: `github.com`, `gitlab.com`, `codeberg.org`, or `:unknown`. Only `github.com` has a working adapter at v0.1.
|
|
60
|
+
|
|
61
|
+
### `HostAdapter` (interface) and `GitHubAdapter` (implementation)
|
|
62
|
+
|
|
63
|
+
Input: a `Project` plus, for some methods, an auth token.
|
|
64
|
+
Output: issues, CONTRIBUTING content, fork results.
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
def issues(project, labels:) # public, no auth needed
|
|
68
|
+
def community_profile(project) # public, no auth needed
|
|
69
|
+
def file_contents(project, path) # public, no auth needed
|
|
70
|
+
def fork(project) # auth required
|
|
71
|
+
def already_forked?(project) # auth required
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`GitHubAdapter` checks for a cached token before any auth-required call. If there's no token, it raises `AuthRequired` with the host name. The TUI catches this through its message machinery and triggers the device flow. See [ADR-0001](adr/0001-just-in-time-auth.md).
|
|
75
|
+
|
|
76
|
+
Adding a new host (GitLab, Codeberg) means writing a new adapter that conforms to the interface. The TUI doesn't change.
|
|
77
|
+
|
|
78
|
+
### `Auth`
|
|
79
|
+
|
|
80
|
+
Implements the OAuth 2.0 Device Authorization Grant against `github.com`. Stores tokens at `~/.config/gem-contribute/auth.json` (mode 0600). Token cache is keyed by host so multi-host support drops in cleanly.
|
|
81
|
+
|
|
82
|
+
The OAuth App is registered under the maintainer's account. Client ID is a public constant in source — there is no client secret in device flow, by design. See [ADR-0004](adr/0004-device-flow-auth.md).
|
|
83
|
+
|
|
84
|
+
## TUI layer
|
|
85
|
+
|
|
86
|
+
The TUI is a [Rooibos](https://www.rooibos.run/) application. Rooibos provides Model-View-Update (Elm-style) on top of `ratatui_ruby`, plus async Commands and snapshot testing. See [ADR-0008](adr/0008-rooibos-tui-framework.md) for why.
|
|
87
|
+
|
|
88
|
+
If you've never used Rooibos: read its [Why Rooibos](https://www.rooibos.run/docs/v0.7/doc/getting_started/why_rooibos_md.html) and the Rails-developer guide before changing TUI code. Twenty minutes of orientation saves hours of writing-against-the-grain.
|
|
89
|
+
|
|
90
|
+
### Mental model
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
┌────────┐ message ┌─────────────────┐
|
|
94
|
+
│ User │ ───────────────▶│ Update │
|
|
95
|
+
└────────┘ │ (model, msg) → │
|
|
96
|
+
▲ │ model | [model,│
|
|
97
|
+
│ │ command] │
|
|
98
|
+
│ keys, mouse └────────┬────────┘
|
|
99
|
+
│ │
|
|
100
|
+
│ ├── new model
|
|
101
|
+
│ │ │
|
|
102
|
+
│ │ ▼
|
|
103
|
+
│ │ ┌───────┐
|
|
104
|
+
│ │ │ View │ ───── render ──┐
|
|
105
|
+
│ │ └───────┘ │
|
|
106
|
+
│ │ │
|
|
107
|
+
│ └── command (async) │
|
|
108
|
+
│ │ │
|
|
109
|
+
│ │ http, system, │
|
|
110
|
+
│ │ wait, etc. │
|
|
111
|
+
│ │ │
|
|
112
|
+
│ ▼ │
|
|
113
|
+
│ message (back to Update) │
|
|
114
|
+
│ │
|
|
115
|
+
└─── terminal ◀──────────────────────────────────────────────┘
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
State lives in one place. Updates flow in one direction. Async work happens via Commands and reports back as messages.
|
|
119
|
+
|
|
120
|
+
### Fragments
|
|
121
|
+
|
|
122
|
+
The app is composed as a tree of Rooibos fragments. Each fragment has its own `Init`, `Model`, `View`, and `Update`. Parents compose children using Rooibos's `Router` DSL.
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
GemContribute (parent / router)
|
|
126
|
+
├── ProjectList [project list view]
|
|
127
|
+
├── IssueList [issue list for selected project]
|
|
128
|
+
├── IssueDetail [issue body, labels, action keys]
|
|
129
|
+
├── ContributingViewer [rendered CONTRIBUTING.md]
|
|
130
|
+
└── AuthOverlay [device flow prompt — can fire over any view]
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The `AuthOverlay` is a fragment that renders on top of whatever view is active when an `:auth_required` message fires. When auth succeeds, the overlay closes and the original action retries.
|
|
134
|
+
|
|
135
|
+
### Commands
|
|
136
|
+
|
|
137
|
+
All async work is a Rooibos Command. We never spawn a thread directly.
|
|
138
|
+
|
|
139
|
+
| What | Command |
|
|
140
|
+
|-----------------------------------------|--------------------------------------------------|
|
|
141
|
+
| Fetch issues for a project | `Command.http(:get, url, :got_issues)` |
|
|
142
|
+
| Run device-flow auth poll | `Command.http(:post, url, :got_token_or_pending)`|
|
|
143
|
+
| Wait between auth polls | `Command.wait(interval, :poll_again)` |
|
|
144
|
+
| Fork a repo via API | `Command.http(:post, url, :forked)` |
|
|
145
|
+
| Clone a forked repo | `Command.system("git clone …", :cloned)` |
|
|
146
|
+
| Create a working branch | `Command.system("git checkout -b …", :branched)` |
|
|
147
|
+
| Open the project in `$EDITOR` | `Command.open(path)` |
|
|
148
|
+
|
|
149
|
+
Each command produces a message. `Update` handles the message the same way it handles a key press. There is no other concurrency model in this app.
|
|
150
|
+
|
|
151
|
+
### Messages and pattern matching
|
|
152
|
+
|
|
153
|
+
`Update` is a single function that pattern-matches on incoming messages. Example shape:
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
Update = -> (message, model) {
|
|
157
|
+
case message
|
|
158
|
+
in :fork_pressed
|
|
159
|
+
[model.with(forking: true), Adapters::GitHub.fork_command(model.current_project)]
|
|
160
|
+
in { type: :http, envelope: :forked, status: 201, body: }
|
|
161
|
+
fork_data = JSON.parse(body, symbolize_names: true)
|
|
162
|
+
clone_cmd = GitWorker.clone_command(fork_data[:clone_url], envelope: :cloned)
|
|
163
|
+
[model.with(fork_data:), clone_cmd]
|
|
164
|
+
in { type: :http, envelope: :forked, status: 401 }
|
|
165
|
+
[model.with(pending_action: :fork_pressed), AuthFlow.start_command]
|
|
166
|
+
in { type: :system, envelope: :cloned, status: 0, stdout: }
|
|
167
|
+
[model.with(local_path: extract_path(stdout)), GitWorker.branch_command(...)]
|
|
168
|
+
# … and so on
|
|
169
|
+
end
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
This is the entirety of the control flow. Async work is just messages with envelopes. Errors are messages too. The shape never changes.
|
|
174
|
+
|
|
175
|
+
## What's deliberately not here
|
|
176
|
+
|
|
177
|
+
- **Label normalization.** Maintainers chose those labels. Render them. ([ADR-0005](adr/0005-render-labels-verbatim.md))
|
|
178
|
+
- **CONTRIBUTING parsing.** Show it. Let the user read it. ([ADR-0007](adr/0007-display-contributing-verbatim.md))
|
|
179
|
+
- **PR creation from inside the TUI.** Out of scope; the user writes code in their editor and pushes from their terminal.
|
|
180
|
+
- **Private gems / private repos.** Possible later. Out of scope at v0.1; the auth scope is `public_repo` only.
|
|
181
|
+
- **Bundler plugin packaging.** Standalone gem at v0.1. ([ADR-0006](adr/0006-standalone-gem-not-plugin.md))
|
|
182
|
+
- **A `Worker` module.** Earlier drafts had one. Fork-clone-branch is a sequence of Commands emitted from `Update`; there's no separate orchestrator class. The state machine *is* the orchestrator.
|
|
183
|
+
- **Direct threading.** All async work goes through Rooibos Commands. ([ADR-0008](adr/0008-rooibos-tui-framework.md))
|
|
184
|
+
|
|
185
|
+
## Configuration
|
|
186
|
+
|
|
187
|
+
`~/.config/gem-contribute/config.yml`:
|
|
188
|
+
|
|
189
|
+
```yaml
|
|
190
|
+
clone_root: ~/code/oss
|
|
191
|
+
preferred_labels:
|
|
192
|
+
- good first issue
|
|
193
|
+
- good-first-issue
|
|
194
|
+
- help wanted
|
|
195
|
+
- documentation
|
|
196
|
+
hosts:
|
|
197
|
+
github.com:
|
|
198
|
+
enabled: true
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Everything has a default. The config file is created on first run.
|
|
202
|
+
|
|
203
|
+
## Caching
|
|
204
|
+
|
|
205
|
+
| What | Where | TTL |
|
|
206
|
+
|--------------------------------|------------------------------------|-----------|
|
|
207
|
+
| RubyGems source URLs | `~/.cache/gem-contribute/gems/` | 7 days |
|
|
208
|
+
| Issue lists | `~/.cache/gem-contribute/issues/` | 5 minutes |
|
|
209
|
+
| Community profiles | `~/.cache/gem-contribute/repos/` | 1 day |
|
|
210
|
+
| File contents (CONTRIBUTING) | `~/.cache/gem-contribute/files/` | 1 day |
|
|
211
|
+
|
|
212
|
+
`gem-contribute --refresh` invalidates all caches. Cache misses degrade gracefully — if the network is down and the cache is empty, the TUI shows what it has and reports the gap honestly.
|
|
213
|
+
|
|
214
|
+
## Testing strategy
|
|
215
|
+
|
|
216
|
+
ADR-0008 changes this materially from earlier drafts. Because `Update` is a pure function and Rooibos provides snapshot helpers, "test the TUI" goes from impractical to easy.
|
|
217
|
+
|
|
218
|
+
- **Unit tests** for parsers, resolvers, and adapters. Adapters use VCR cassettes; cassettes are committed.
|
|
219
|
+
- **`Update` tests** for every fragment. Pure function in, pure function out. Cover at minimum: every key handler, every command-result message, every error path.
|
|
220
|
+
- **View tests** for color and modifier assertions on rendered output. Verify that preferred labels are highlighted, that error states render in red, etc.
|
|
221
|
+
- **System tests** for full-flow scenarios. Inject keys, run the app to a quiescent state, snapshot the result. Snapshots are committed.
|
|
222
|
+
- **Integration test** against a single live gem (`mailcatcher` is small and friendly) — runs only when `GEM_CONTRIBUTE_INTEGRATION=1` is set. Catches real-world breakage of the adapter.
|
|
223
|
+
|
|
224
|
+
The Rooibos snapshot tooling normalizes dynamic content (timestamps, paths in `~/code/oss/...`) so snapshot diffs stay legible. Run `UPDATE_SNAPSHOTS=1 rake test` to regenerate baselines.
|
|
225
|
+
|
|
226
|
+
## Roadmap (non-promises)
|
|
227
|
+
|
|
228
|
+
**v0.1 (workshop):** GitHub-only, JIT auth, fork-clone-branch working, four primary fragments, Rooibos throughout, snapshot tests for the main flows.
|
|
229
|
+
|
|
230
|
+
**v0.2:** Better empty states, rate-limit display in the status bar, `r` keybinding to refresh the current view, keyboard help overlay (an additional fragment).
|
|
231
|
+
|
|
232
|
+
**v0.3:** GitLab adapter. The data-layer/TUI-layer split above is the bet that this is a weekend project, not a rewrite.
|
|
233
|
+
|
|
234
|
+
**Maybe-never:** Codeberg, sourcehut, private repos, PR creation, label normalization, AI-anything, Bundler plugin, RubyGems plugin (a thin lazy-loading shim is the most we'd consider).
|