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,75 @@
|
|
|
1
|
+
# ADR 0014: Ship Bundler and RubyGems plugins as v1 interfaces
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted
|
|
4
|
+
**Date:** 2026-05-03
|
|
5
|
+
**Amends:** [ADR-0006](0006-standalone-gem-not-plugin.md), [ADR-0012](0012-output-free-service-objects-three-interface-architecture.md)
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
[ADR-0006](0006-standalone-gem-not-plugin.md) (2026-04-27) decided to ship as a standalone gem rather than a Bundler plugin. The primary reason was workshop scope — Bundler plugin authoring would distract attendees from the actual learning objectives (Ratatui, OAuth, GitHub's API). It explicitly left the door open: *"A future ADR can revisit this if the tool sees real adoption and the plugin UX becomes the bottleneck."*
|
|
10
|
+
|
|
11
|
+
[ADR-0012](0012-output-free-service-objects-three-interface-architecture.md) (2026-05-03 morning) added a RubyGems plugin (`gem contribute`) as a third interface, alongside the standalone CLI and the (then-bubbletea) TUI. It noted that the Bundler plugin decision in ADR-0006 was unchanged.
|
|
12
|
+
|
|
13
|
+
The Blue Ridge Ruby workshop concluded 2026-05-02. ADR-0006's workshop-scope concern is no longer load-bearing.
|
|
14
|
+
|
|
15
|
+
Separately: the v1 release goal now includes both `bundle contribute` *and* `gem contribute` as discoverable entry points, alongside `gem-contribute` itself. This makes the tool reachable from whatever invocation surface a user is already in.
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
Ship three entry points for v1:
|
|
20
|
+
|
|
21
|
+
1. **`gem-contribute`** — standalone CLI binary. Bare invocation (no subcommand) launches the Rooibos TUI. Subcommands run as CLI verbs.
|
|
22
|
+
2. **`bundle contribute`** — Bundler plugin. CLI-only. Bare invocation runs a default summary verb (TBD: `scan` vs `list all`). Subcommands run as CLI verbs.
|
|
23
|
+
3. **`gem contribute`** — RubyGems plugin. CLI-only. Same shape as `bundle contribute`.
|
|
24
|
+
|
|
25
|
+
All three entry points ship in **a single gem** (`gem-contribute`). One `gem install gem-contribute` registers the standalone binary, the Bundler plugin, and the RubyGems plugin.
|
|
26
|
+
|
|
27
|
+
## Reasoning
|
|
28
|
+
|
|
29
|
+
**The workshop scope-creep argument has expired.** ADR-0006's central rejection was "plugin authoring would distract workshop attendees." Workshop is done; the v1 audience is end users and contributors, not workshop attendees. The remaining ADR-0006 reasoning (UX nicety of `bundle X`) actually *supports* shipping plugins.
|
|
30
|
+
|
|
31
|
+
**Plugin entry points are CLI-only, by design.** Bundler and RubyGems plugin ecosystems are built around CLI subcommands, not interactive TUIs. Users running `bundle contribute` expect the same kind of behavior as `bundle exec`, `bundle install`, etc. — text in, text out. The TUI is a property of the standalone binary; the plugins delegate into the same service-layer entry points the CLI uses.
|
|
32
|
+
|
|
33
|
+
This has a useful architectural consequence: the plugin entry points never need to load Rooibos or `ratatui_ruby`. Plugin install stays lightweight, and plugin invocations don't pay the TUI startup cost.
|
|
34
|
+
|
|
35
|
+
**One gem rather than three.** ADR-0012 sketched a future `rubygems-contribute` gem. Three gems would follow Ruby ecosystem convention (`bundler-X`, `rubygems-X`) but tripples release ceremony, version coordination, and CHANGELOG maintenance. For a project this size, that cost is not earned. One gem with three entry points: one `gem install`, one CHANGELOG, one version, all three interfaces work.
|
|
36
|
+
|
|
37
|
+
**ADR-0012's three-interface framing carries over unchanged.** The service layer (output-free, returns `Result`) is what enables three interfaces to share code. ADR-0014 doesn't add a fourth interface; it confirms the third (Bundler plugin) and locks the packaging to a single gem.
|
|
38
|
+
|
|
39
|
+
## Alternatives considered
|
|
40
|
+
|
|
41
|
+
- **Stay standalone-only (ADR-0006 unmodified).** Rejected: the original justification (workshop scope) no longer applies, and one of v1's product goals is to make the tool reachable from `bundle X` and `gem X` invocations.
|
|
42
|
+
- **Three separate gems.** Rejected: triples release ceremony for marginal architectural cleanliness. The plugin shims would be tiny — a Bundler `Plugin::API` registration and a `Gem::Command` subclass — not worth their own gemspec, version, and changelog.
|
|
43
|
+
- **Ship `bundle contribute` only, defer `gem contribute` to v1.x.** Rejected as worse-of-both-worlds: same release work, half the surface area covered. They're symmetric pieces of work.
|
|
44
|
+
- **TUI in plugins too.** Rejected: not idiomatic for the Bundler/RubyGems plugin ecosystems, doubles the per-invocation startup cost, and the standalone binary already serves the "I want the TUI" use case.
|
|
45
|
+
|
|
46
|
+
## Consequences
|
|
47
|
+
|
|
48
|
+
**On the gemspec:**
|
|
49
|
+
- Add `plugins.rb` (Bundler plugin entry point) per Bundler plugin convention.
|
|
50
|
+
- Add `rubygems_plugin.rb` (RubyGems plugin entry point) per RubyGems plugin convention.
|
|
51
|
+
- The two entry points register their respective subcommand classes (or delegate to a shared dispatch table).
|
|
52
|
+
|
|
53
|
+
**On `lib/gem_contribute/cli.rb`:**
|
|
54
|
+
- The dispatch table becomes the single source of truth for verb registration.
|
|
55
|
+
- Bare-arg behavior diverges per entry point: `gem-contribute` launches TUI; `bundle contribute` and `gem contribute` run a default CLI verb.
|
|
56
|
+
- Consider `dry-cli` to formalize multi-entry-point command registration (deferred decision; see OPEN_QUESTIONS Q6 sub-question).
|
|
57
|
+
|
|
58
|
+
**On the Bundler plugin entry point:** must not require Rooibos or `ratatui_ruby` at load time. TUI loading is gated to the standalone-binary entry point only.
|
|
59
|
+
|
|
60
|
+
**On `ADR-0006`:** status updated to note ADR-0014 amends it. The standalone-gem decision stands; the no-Bundler-plugin decision is reversed.
|
|
61
|
+
|
|
62
|
+
**On `ADR-0012`:** status updated to note ADR-0014 amends it. The three-interface architecture is preserved; the planned separate `rubygems-contribute` gem is replaced by a single `gem-contribute` gem with three entry points.
|
|
63
|
+
|
|
64
|
+
**On `docs/design-interface-layer.md`:** "gem plugin pipeline" section needs updating — the plugin shim is internal to the `gem-contribute` gem, not a separate `rubygems-contribute` gem. Same change for the Bundler plugin section (which doesn't yet exist; needs adding).
|
|
65
|
+
|
|
66
|
+
**On testing:** add at least one smoke test per plugin entry point that proves the plugin registers and dispatches a verb without booting the TUI. Implementation tests live in the existing CLI verb specs (verbs aren't aware of which entry point invoked them).
|
|
67
|
+
|
|
68
|
+
**On release:** the `bundle plugin install gem-contribute` and `gem install gem-contribute` paths both need verification before v1 ships. Add to Phase 6 acceptance.
|
|
69
|
+
|
|
70
|
+
## What this *doesn't* change
|
|
71
|
+
|
|
72
|
+
- ADR-0001 through ADR-0005, ADR-0007 (data layer, display rules). Plugin entry points reuse the same service layer.
|
|
73
|
+
- ADR-0009 (top-level namespace). Plugins live under `GemContribute::` like everything else.
|
|
74
|
+
- ADR-0011 (HostAdapter owns host verbs). Service layer; orthogonal.
|
|
75
|
+
- ADR-0013 (Rooibos as TUI framework). The plugins are CLI-only; framework choice doesn't reach them.
|
data/docs/adr/README.md
CHANGED
|
@@ -11,10 +11,15 @@ Format: [Michael Nygard's template](https://github.com/joelparkerhenderson/archi
|
|
|
11
11
|
- [0003 — Prefer `bug_tracker_uri` over `source_code_uri`](0003-issue-tracker-preference.md)
|
|
12
12
|
- [0004 — Use OAuth Device Flow, not PATs](0004-device-flow-auth.md)
|
|
13
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)
|
|
14
|
+
- [0006 — Ship as a standalone gem, not a Bundler plugin](0006-standalone-gem-not-plugin.md) — Bundler-plugin decision reversed by 0014; standalone-gem decision stands
|
|
15
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)
|
|
16
|
+
- [0008 — Use Rooibos for the TUI layer](0008-rooibos-tui-framework.md) — superseded by 0010, substance restored by 0013
|
|
17
17
|
- [0009 — Top-level namespace is `GemContribute`](0009-top-level-namespace.md)
|
|
18
|
+
- [0010 — Use Charm-Ruby (bubbletea + lipgloss) for the TUI layer](0010-charm-ruby-tui-framework.md) — superseded by 0013
|
|
19
|
+
- [0011 — HostAdapter owns host verbs; Operations compose them; CLI verbs compose Operations](0011-host-adapter-owns-host-verbs.md)
|
|
20
|
+
- [0012 — Output-free service objects, dry-monads Result contract, three-interface architecture](0012-output-free-service-objects-three-interface-architecture.md) — packaging amended by 0014; service-layer contract stands
|
|
21
|
+
- [0013 — Revert TUI framework to Rooibos](0013-revert-to-rooibos.md) — supersedes 0010, restores 0008's substance
|
|
22
|
+
- [0014 — Ship Bundler and RubyGems plugins as v1 interfaces](0014-ship-bundler-and-rubygems-plugins.md) — amends 0006 and 0012
|
|
18
23
|
|
|
19
24
|
## When to add an ADR
|
|
20
25
|
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# Interface Layer Design
|
|
2
|
+
|
|
3
|
+
This document describes the multi-interface architecture introduced by [ADR-0012](adr/0012-output-free-service-objects-three-interface-architecture.md). It is the reference for all work that touches the boundary between service objects and the three output vectors: CLI, TUI, and gem plugin. Read it before changing anything in `lib/gem_contribute/cli/` or `lib/gem_contribute/operations/`.
|
|
4
|
+
|
|
5
|
+
For the broader system architecture — parsers, resolvers, the host adapter interface, caching — see [`design.md`](design.md).
|
|
6
|
+
|
|
7
|
+
## The problem with the current boundary
|
|
8
|
+
|
|
9
|
+
`Operations::Fork` and `Operations::Clone` accept `stdout:` and print progress during `call`:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
def call(adapter:, project:)
|
|
13
|
+
@stdout.puts "Forking #{project.owner}/#{project.repo}..."
|
|
14
|
+
fork = adapter.fork(project)
|
|
15
|
+
@stdout.puts fork.reused ? " Reusing existing fork." : " Forked → ..."
|
|
16
|
+
Result.new(...)
|
|
17
|
+
end
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This works for a single CLI, but breaks across three interfaces:
|
|
21
|
+
|
|
22
|
+
- The **TUI** has no output stream. Progress is model state rendered by `View`, not a string printed to stdout. There is no clean way to route `@stdout.puts` into a Rooibos model update.
|
|
23
|
+
- The **gem plugin** needs to own its own output formatting and cannot share stdout injection with the CLI.
|
|
24
|
+
- **Tests** must assert on stdout side effects instead of return values, coupling test setup to I/O concerns.
|
|
25
|
+
|
|
26
|
+
The same problem appears in `Workflow#build_adapter`, which returns `nil` and prints to stderr on failure — two concerns collapsed into one method.
|
|
27
|
+
|
|
28
|
+
## Target architecture
|
|
29
|
+
|
|
30
|
+
Three interface layers share one service layer. The service layer produces no output. Each interface layer translates `Result` values into the output appropriate for its medium.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
Service Layer
|
|
34
|
+
┌────────────────────────────────────────────────────┐
|
|
35
|
+
│ Operations::Fork Operations::Clone │
|
|
36
|
+
│ Operations::FixPipeline Resolver │
|
|
37
|
+
│ HostAdapter Auth Git │
|
|
38
|
+
│ │
|
|
39
|
+
│ Returns Success(Result) | Failure(reason) │
|
|
40
|
+
│ No stdout. No stderr. No exceptions across layers.│
|
|
41
|
+
└───────────────┬───────────────────┬────────────────┘
|
|
42
|
+
│ │ │
|
|
43
|
+
▼ ▼ ▼
|
|
44
|
+
┌───────────────┐ ┌───────────────┐ ┌──────────────┐
|
|
45
|
+
│ CLI Pipeline │ │ TUI Pipeline │ │ gem plugin │
|
|
46
|
+
│ │ │ │ │ Pipeline │
|
|
47
|
+
│ Output:: │ │ Commands + │ │ │
|
|
48
|
+
│ Standard │ │ Update │ │ reuses CLI │
|
|
49
|
+
│ tty-spinner │ │ │ │ pipeline │
|
|
50
|
+
│ tty-prompt │ │ │ │ │
|
|
51
|
+
└───────────────┘ └───────────────┘ └──────────────┘
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The three interface pipelines are entirely separate. A change to CLI output formatting has zero effect on the TUI. A new service object needs no knowledge of any interface.
|
|
55
|
+
|
|
56
|
+
## Service layer
|
|
57
|
+
|
|
58
|
+
### Result types
|
|
59
|
+
|
|
60
|
+
Every operation returns a `dry-monads` `Result`:
|
|
61
|
+
|
|
62
|
+
- `Success(value)` on the happy path — `value` is a `Data` struct.
|
|
63
|
+
- `Failure(reason)` for expected failure conditions — `reason` is a symbol or tagged tuple.
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
Success(Operations::Fork::Result.new(clone_url:, fork_url:, upstream_url:, viewer:, reused:))
|
|
67
|
+
|
|
68
|
+
Failure(:unauthenticated)
|
|
69
|
+
Failure(:adapter_error, "rate limit exceeded")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Typed exceptions (`AuthRequired`, `AdapterError`) are not used as cross-layer signals. They may be raised and rescued within a single operation but do not propagate to callers.
|
|
73
|
+
|
|
74
|
+
### No I/O
|
|
75
|
+
|
|
76
|
+
Operations accept no `stdout:` or `stderr:` parameter. The `# rubocop:disable Metrics/ParameterLists` suppressions in the current `CLI::Fork` and `CLI::Fix` initializers are a symptom of this pattern — the extra `stdout:` and `stderr:` parameters inflate every constructor. `dry-initializer` replaces the verbose initializer pattern and the suppressions go away:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
class Operations::Fork
|
|
80
|
+
extend Dry::Initializer
|
|
81
|
+
# No stdout:. No stderr:. Pure computation.
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Multi-step pipelines
|
|
86
|
+
|
|
87
|
+
The `fix` flow (fork → clone → branch → announce) is a `dry-operation` pipeline. Each step receives a shared context, enriches it, and returns `Success(enriched)` or `Failure(reason)`. Failure at any step short-circuits the chain — no early returns, no nil checks.
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
class Operations::FixPipeline
|
|
91
|
+
include Dry::Operation
|
|
92
|
+
|
|
93
|
+
def call(adapter:, project:, issue:, viewer:)
|
|
94
|
+
fork_result = step Operations::Fork.new.call(adapter:, project:)
|
|
95
|
+
clone_result = step Operations::Clone.new.call(adapter:, project:, fork_result:)
|
|
96
|
+
branch_name = step Operations::Branch.new.call(path: clone_result.path, issue:)
|
|
97
|
+
step Operations::Announce.new.call(adapter:, project:, issue:, viewer:, was_resuming: clone_result.reused)
|
|
98
|
+
Success({ fork: fork_result, clone: clone_result, branch: branch_name })
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Both the CLI and TUI call `FixPipeline` rather than wiring the steps themselves.
|
|
104
|
+
|
|
105
|
+
### `Workflow#build_adapter`
|
|
106
|
+
|
|
107
|
+
Currently returns `nil` and prints to stderr. Under the new contract:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
def build_adapter
|
|
111
|
+
token = @store.token_for("github.com")
|
|
112
|
+
return Failure(:unauthenticated) if token.nil?
|
|
113
|
+
|
|
114
|
+
Success(@adapter_factory.call(token: token))
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Callers pattern-match on the result; no output side effect in the mixin.
|
|
119
|
+
|
|
120
|
+
## CLI pipeline
|
|
121
|
+
|
|
122
|
+
### `Output::Standard`
|
|
123
|
+
|
|
124
|
+
All CLI verbs use `Output::Standard` instead of raw `@stdout`/`@stderr`. It wraps both streams and exposes a semantic interface:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
module GemContribute
|
|
128
|
+
module Output
|
|
129
|
+
class Standard
|
|
130
|
+
def initialize(out: $stdout, err: $stderr)
|
|
131
|
+
@out = out
|
|
132
|
+
@err = err
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def info(message) = @out.puts(message)
|
|
136
|
+
def warn(message) = @err.puts("warning: #{message}")
|
|
137
|
+
def error(message) = @err.puts(message)
|
|
138
|
+
def progress(message) # tty-spinner in interactive terminals;
|
|
139
|
+
# falls back to info when stdout is not a TTY
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
class Null
|
|
143
|
+
def info(_) = nil
|
|
144
|
+
def warn(_) = nil
|
|
145
|
+
def error(_) = nil
|
|
146
|
+
def progress(_) = nil
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`#progress` is the one method that behaves differently from a plain `puts`. Long operations (fork, clone) show a spinner in interactive terminals. `tty-spinner` detects non-TTY environments automatically and falls back to a plain line — no caller checks `$stdout.tty?`.
|
|
153
|
+
|
|
154
|
+
### Interactive prompts
|
|
155
|
+
|
|
156
|
+
`Init` is the one command that reads input. Its current `stdout.print` + injected `@gets` lambda is replaced by `tty-prompt`:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
prompt = TTY::Prompt.new(input: @input, output: @output)
|
|
160
|
+
chosen = prompt.ask("Where should I clone repos?", default: DEFAULT_SUGGESTION)
|
|
161
|
+
proceed = prompt.yes?("Authenticate with GitHub now?")
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`TTY::Prompt.new(input:, output:)` provides the same test injection story the current `gets:` lambda provides, with proper default-value display, Y/n handling, and non-TTY fallback built in.
|
|
165
|
+
|
|
166
|
+
### How CLI verbs change
|
|
167
|
+
|
|
168
|
+
CLI verbs print around service calls. The operation returns a `Result`; the verb interprets it:
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
def execute(adapter, project, flags)
|
|
172
|
+
@output.progress("Forking #{project.owner}/#{project.repo}...")
|
|
173
|
+
|
|
174
|
+
case @fork_op.call(adapter: adapter, project: project)
|
|
175
|
+
in Success(fork_result)
|
|
176
|
+
@output.info(fork_result.reused ? " Reusing existing fork." : " Forked.")
|
|
177
|
+
continue_with(fork_result, flags)
|
|
178
|
+
in Failure(:unauthenticated)
|
|
179
|
+
@output.error("Not authenticated. Run `gem-contribute auth login` first.")
|
|
180
|
+
1
|
|
181
|
+
in Failure(:adapter_error, message)
|
|
182
|
+
@output.error(message)
|
|
183
|
+
1
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The `with_workflow_rescues` wrapper in `Workflow` is retired — pattern matching on `Result` replaces it.
|
|
189
|
+
|
|
190
|
+
## TUI pipeline
|
|
191
|
+
|
|
192
|
+
The TUI never uses an `Output` object. A Rooibos Command wraps the service call, runs it off-thread, and returns a message to `Update`:
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
def fix_command(adapter:, project:, issue:, viewer:)
|
|
196
|
+
-> {
|
|
197
|
+
result = Operations::FixPipeline.new.call(
|
|
198
|
+
adapter: adapter, project: project, issue: issue, viewer: viewer
|
|
199
|
+
)
|
|
200
|
+
{ type: :fix_completed, result: result }
|
|
201
|
+
}
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`Update` pattern-matches on the message:
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
def update(msg, model)
|
|
209
|
+
case msg
|
|
210
|
+
in { type: :fix_completed, result: Success({ fork:, clone:, branch: }) }
|
|
211
|
+
model.with(local_path: clone.path, branch_name: branch, status: :done)
|
|
212
|
+
in { type: :fix_completed, result: Failure(:unauthenticated) }
|
|
213
|
+
[model.with(pending_action: :fix), AuthOverlay.start_command]
|
|
214
|
+
in { type: :fix_completed, result: Failure(:adapter_error, message) }
|
|
215
|
+
model.with(error: message)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The service object returns a `Result`; the Command wraps it in a typed message; `Update` renders it as model state. No output object, no stdout, no cross-layer exceptions.
|
|
221
|
+
|
|
222
|
+
## Plugin pipelines
|
|
223
|
+
|
|
224
|
+
Per [ADR-0014](adr/0014-ship-bundler-and-rubygems-plugins.md), v1 ships three entry points in a single `gem-contribute` gem: the standalone `gem-contribute` binary, a Bundler plugin (`bundle contribute`), and a RubyGems plugin (`gem contribute`). Both plugins are CLI-only; the TUI is a property of the standalone binary.
|
|
225
|
+
|
|
226
|
+
- **Bundler plugin** — `plugins.rb` at the gem root, per Bundler convention. Registers a `bundle contribute` command and delegates to the same dispatch table the standalone CLI uses.
|
|
227
|
+
- **RubyGems plugin** — `rubygems_plugin.rb`, per RubyGems convention. Registers a `Gem::Command` subclass for `gem contribute` and delegates the same way.
|
|
228
|
+
|
|
229
|
+
Both plugin entry points MUST NOT require Rooibos or `ratatui_ruby`. The TUI gets loaded only by the standalone-binary entry point, which keeps plugin install lightweight and plugin invocations fast.
|
|
230
|
+
|
|
231
|
+
When `dry-cli` is added (alongside the plugin shims), it replaces the hand-rolled `COMMANDS` dispatch in `cli.rb` with declarative command registration. All three entry points register the same underlying commands against their respective hosts — the commands themselves do not change.
|
|
232
|
+
|
|
233
|
+
The plugin work is deferred until the service layer and CLI pipeline are clean (this document's Phases 1 and 2) and the TUI lands ([ROADMAP](ROADMAP.md) Phase 3).
|
|
234
|
+
|
|
235
|
+
## New dependencies
|
|
236
|
+
|
|
237
|
+
| Gem | Role | Phase |
|
|
238
|
+
|---|---|---|
|
|
239
|
+
| `dry-monads` | `Success`/`Failure` Result types | 1 |
|
|
240
|
+
| `dry-operation` | Multi-step pipeline composition for `fix` | 1 |
|
|
241
|
+
| `dry-initializer` | Clean option declarations; removes rubocop suppressions | 1 |
|
|
242
|
+
| `tty-spinner` | Spinner in `Output::Standard#progress` | 2 |
|
|
243
|
+
| `tty-prompt` | Interactive prompts in `Init` | 2 |
|
|
244
|
+
| `dry-cli` | Command registration across all three entry points | 4/5 (with plugin shims) |
|
|
245
|
+
|
|
246
|
+
Pin all new dependencies to a minor version (`~> x.y`). Bump deliberately; record significant bumps in an ADR note.
|
|
247
|
+
|
|
248
|
+
## Migration sequence
|
|
249
|
+
|
|
250
|
+
### Phase 1 — Service layer
|
|
251
|
+
|
|
252
|
+
1. Add `dry-monads`, `dry-operation`, `dry-initializer` to the gemspec.
|
|
253
|
+
2. Remove `stdout:` from `Operations::Fork` and `Operations::Clone`. Add `reused:` to `Clone::Result`.
|
|
254
|
+
3. Convert both operations to return `Success(Result)` | `Failure(reason)`.
|
|
255
|
+
4. Convert `Workflow#build_adapter` to return `Success(adapter)` | `Failure(:unauthenticated)`.
|
|
256
|
+
5. Build `Operations::FixPipeline` using `dry-operation`.
|
|
257
|
+
6. Replace long initializers in `CLI::Fork` and `CLI::Fix` with `dry-initializer` option declarations.
|
|
258
|
+
7. Update all callers to pattern-match on `Result`. Retire `with_workflow_rescues`.
|
|
259
|
+
|
|
260
|
+
### Phase 2 — CLI pipeline
|
|
261
|
+
|
|
262
|
+
1. Introduce `Output::Standard` and `Output::Null`.
|
|
263
|
+
2. Replace raw `@stdout`/`@stderr` in all CLI verbs with `@output`.
|
|
264
|
+
3. Add `tty-spinner` for `Output::Standard#progress`.
|
|
265
|
+
4. Replace `Init`'s `stdout.print` + `@gets` with `tty-prompt`.
|
|
266
|
+
|
|
267
|
+
### Phases 4 and 5 — Bundler and RubyGems plugins
|
|
268
|
+
|
|
269
|
+
Per ADR-0014 and the [ROADMAP](ROADMAP.md), the gem-plugin work is split into two ROADMAP phases (one per plugin) and lives within the `gem-contribute` gem rather than as separate `bundler-contribute` / `rubygems-contribute` gems.
|
|
270
|
+
|
|
271
|
+
1. Add `dry-cli`. Replace the `COMMANDS` dispatch in `cli.rb` with `dry-cli` command registration so multiple entry points can hang off it.
|
|
272
|
+
2. Add `plugins.rb` at the gem root (Bundler plugin entry point). Register a `bundle contribute` command that dispatches into the same registration table.
|
|
273
|
+
3. Add `rubygems_plugin.rb` (RubyGems plugin entry point). Register a `Gem::Command` subclass that does the same.
|
|
274
|
+
4. Smoke-test both plugin install paths in CI.
|
|
275
|
+
|
|
276
|
+
Each ADR-0012 phase is independently releasable. Phase 1 has no user-visible behaviour change. Phase 2 changes the look of progress output and prompts. The plugin phases add new entry points.
|
|
277
|
+
|
|
278
|
+
## Testing strategy
|
|
279
|
+
|
|
280
|
+
**Service layer:** pure function in, `Result` out. Every operation gets unit tests that assert on `Success`/`Failure` values directly. No stdout assertion. No mock needed for output. VCR cassettes for adapter-touching operations; committed to the repo.
|
|
281
|
+
|
|
282
|
+
**CLI pipeline:** inject `Output::Null` for tests that don't assert on output; inject a capturing double for tests that do. Assert on captured calls to `#info`/`#error`/`#progress`, not on raw stdout strings.
|
|
283
|
+
|
|
284
|
+
**TUI pipeline:** the Command closure is a plain lambda — call it directly and assert on the returned message hash. `Update` is a pure function — call it with a message and assert on the returned model. Rooibos's snapshot test helpers (per ADR-0008/0013) supplement this for full-flow scenarios.
|
|
285
|
+
|
|
286
|
+
**gem plugin:** thin entry-point tests only. The CLI pipeline tests cover the behaviour.
|
|
287
|
+
|
|
288
|
+
## What doesn't change
|
|
289
|
+
|
|
290
|
+
- The `HostAdapter` interface and `GitHubAdapter` implementation.
|
|
291
|
+
- `Resolver`, `LockfileParser`, `TokenStore`, `Cache` — already output-free.
|
|
292
|
+
- The Auth flow shape. `CLI::Auth` keeps its existing structure; `Workflow#build_adapter`'s change is internal wiring.
|
|
293
|
+
- Caching strategy and TTLs.
|
|
294
|
+
- The Rooibos framework choice (ADR-0013, which superseded ADR-0010).
|
|
295
|
+
- ADR-0005 (render labels verbatim), ADR-0007 (show CONTRIBUTING), ADR-0009 (namespace). None of these touch the interface boundary.
|
data/docs/design.md
CHANGED
|
@@ -61,19 +61,42 @@ The `host` is parsed from the URL: `github.com`, `gitlab.com`, `codeberg.org`, o
|
|
|
61
61
|
### `HostAdapter` (interface) and `GitHubAdapter` (implementation)
|
|
62
62
|
|
|
63
63
|
Input: a `Project` plus, for some methods, an auth token.
|
|
64
|
-
Output: issues, CONTRIBUTING content, fork results.
|
|
64
|
+
Output: issues, CONTRIBUTING content, fork results, host-specific URLs.
|
|
65
65
|
|
|
66
66
|
```ruby
|
|
67
|
-
|
|
68
|
-
def
|
|
69
|
-
def
|
|
70
|
-
def
|
|
71
|
-
def
|
|
67
|
+
# Reads — no auth
|
|
68
|
+
def issues(project, labels:)
|
|
69
|
+
def issue(project, number)
|
|
70
|
+
def issue_comments(project, number)
|
|
71
|
+
def community_profile(project)
|
|
72
|
+
def file_contents(project, path)
|
|
73
|
+
def search_issues(query)
|
|
74
|
+
|
|
75
|
+
# Writes — auth required
|
|
76
|
+
def fork(project) # idempotent, blocks until ready; → ForkResult
|
|
77
|
+
def comment(project, issue:, body:)
|
|
78
|
+
def pull_request_url(upstream, head_owner:, head_branch:, title:, body:)
|
|
79
|
+
|
|
80
|
+
# Identity / URL helpers
|
|
81
|
+
def viewer_login # auth required
|
|
82
|
+
def clone_url(owner, repo) # pure templating, no network
|
|
83
|
+
def repo_url(owner, repo) # pure templating, no network
|
|
72
84
|
```
|
|
73
85
|
|
|
74
86
|
`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
87
|
|
|
76
|
-
|
|
88
|
+
`fork` is idempotent and blocking: if the viewer already owns the fork it returns `reused: true` without a POST; otherwise it POSTs and polls the host until the new fork is reachable. Higher layers don't see the polling. PR creation is not an API call — the adapter's `pull_request_url` returns a host-specific compare/MR URL that gets opened in the browser, so the user reviews the PR text before submitting. See [ADR-0011](adr/0011-host-adapter-owns-host-verbs.md).
|
|
89
|
+
|
|
90
|
+
Adding a new host (GitLab, Codeberg) means writing a new adapter that conforms to the interface — including its own `clone_url` / `repo_url` / `pull_request_url` templates and its own readiness model for `fork`. Operations and CLI don't change.
|
|
91
|
+
|
|
92
|
+
### `Operations::Fork` and `Operations::Clone`
|
|
93
|
+
|
|
94
|
+
Two thin primitives sitting between the adapter and the CLI verbs. They're the bootstrap step `fix` and `fork` share:
|
|
95
|
+
|
|
96
|
+
- `Operations::Fork` calls `adapter.fork(project)` and packages the result with the upstream URL for summary output.
|
|
97
|
+
- `Operations::Clone` clones the fork into `<root>/<owner>/<repo>` (reusing an existing clone if one is there) and adds an `upstream` remote pointing at `adapter.clone_url(upstream)`.
|
|
98
|
+
|
|
99
|
+
The "reuse if `.git` exists" rule and the upstream-remote convention are gem-contribute policy on top of git, not git primitives — that's why they live here rather than in the `Git` wrapper. See [ADR-0011](adr/0011-host-adapter-owns-host-verbs.md).
|
|
77
100
|
|
|
78
101
|
### `Auth`
|
|
79
102
|
|
|
@@ -225,7 +248,7 @@ The Rooibos snapshot tooling normalizes dynamic content (timestamps, paths in `~
|
|
|
225
248
|
|
|
226
249
|
## Roadmap (non-promises)
|
|
227
250
|
|
|
228
|
-
**v0.1 (workshop):** GitHub-only, JIT auth,
|
|
251
|
+
**v0.1 (workshop):** GitHub-only, JIT auth, `fix` working, four primary fragments, Rooibos throughout, snapshot tests for the main flows.
|
|
229
252
|
|
|
230
253
|
**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
254
|
|
data/docs/ideas.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
- Make sure it respects PR templates
|
data/docs/index.md
CHANGED
|
@@ -38,7 +38,7 @@ gem-contribute submit # push, then open the PR compare page in y
|
|
|
38
38
|
|
|
39
39
|
- **v0.1**: a CLI with `scan`, `issues`, `auth`, `fix`, `submit`, and `config`. GitHub-only.
|
|
40
40
|
- **Planned**: a Rooibos TUI that does all of the above as a single keyboard-driven session ([issue #2](https://github.com/cdhagmann/gem-contribute/issues/2)).
|
|
41
|
-
- **Workshop project**: built for [Blue Ridge Ruby 2026](https://blueridgeruby.com).
|
|
41
|
+
- **Workshop project**: built for [Blue Ridge Ruby 2026](https://blueridgeruby.com). [Lightning talk slides →](talk/)
|
|
42
42
|
|
|
43
43
|
## Commands
|
|
44
44
|
|
|
@@ -50,7 +50,7 @@ gem-contribute submit # push, then open the PR compare page in y
|
|
|
50
50
|
| `gem-contribute auth login` | Authenticate with GitHub via OAuth device flow (no token paste, no client secret). |
|
|
51
51
|
| `gem-contribute auth status` | Show whether the cached token is still valid. |
|
|
52
52
|
| `gem-contribute auth logout` | Drop the cached token. |
|
|
53
|
-
| `gem-contribute fix <gem>/<n>` | Fork the gem's repo, clone the fork to `<clone_root>/<owner>/<repo>`, branch from default.
|
|
53
|
+
| `gem-contribute fix <gem>/<n>` | Fork the gem's repo, clone the fork to `<clone_root>/<owner>/<repo>`, branch from default. |
|
|
54
54
|
| `gem-contribute submit` | From inside a clone, push the current branch and open a pre-filled PR compare page in your browser. |
|
|
55
55
|
| `gem-contribute config set <k> <v>` | Persist user preferences. |
|
|
56
56
|
| `gem-contribute config list` | Show current configuration. |
|
data/docs/prep-plan.md
CHANGED
|
@@ -36,14 +36,14 @@ Minimum viable workshop = Stages 1, 2, and 4 done. Stage 3 (the TUI) can become
|
|
|
36
36
|
**Deliberately not in this stage:**
|
|
37
37
|
- No auth code. Anonymous GitHub API only.
|
|
38
38
|
- No TUI. CLI output via plain `puts`.
|
|
39
|
-
- No
|
|
39
|
+
- No `fix` action. That's Stage 2.
|
|
40
40
|
- No Rooibos dependency yet.
|
|
41
41
|
|
|
42
42
|
**Stop here and check in with Chris.** Demo the script against `gem-contribute`'s own `Gemfile.lock`. The output should make Chris want to keep going.
|
|
43
43
|
|
|
44
44
|
### Stage 2 — Auth and the action
|
|
45
45
|
|
|
46
|
-
**Goal:** Add device-flow auth and the
|
|
46
|
+
**Goal:** Add device-flow auth and the `fix` action. Still no TUI; everything is CLI flags. Proves the auth and action layers work.
|
|
47
47
|
|
|
48
48
|
**Acceptance:**
|
|
49
49
|
|
|
@@ -53,7 +53,7 @@ Minimum viable workshop = Stages 1, 2, and 4 done. Stage 3 (the TUI) can become
|
|
|
53
53
|
- [ ] Polling respects `slow_down` errors and the 15-minute device-code expiry
|
|
54
54
|
- [ ] `gem-contribute auth login` and `gem-contribute auth status` CLI commands work
|
|
55
55
|
- [ ] `GitHubAdapter` gains `fork`, `already_forked?` methods that raise `AuthRequired` if no token
|
|
56
|
-
- [ ] A `
|
|
56
|
+
- [ ] A `fix` CLI subcommand takes a `gem/issue_number` argument, performs the full sequence, prints the local path
|
|
57
57
|
- [ ] Unit tests for the auth state machine (the protocol is deterministic — test it)
|
|
58
58
|
- [ ] Integration test gated on `GEM_CONTRIBUTE_INTEGRATION=1` against a small friendly gem
|
|
59
59
|
- [ ] **Use the tool to open one real PR** — even a typo fix in a README. The proof that the architecture works.
|
|
@@ -88,7 +88,7 @@ Minimum viable workshop = Stages 1, 2, and 4 done. Stage 3 (the TUI) can become
|
|
|
88
88
|
- No label normalization (ADR-0005)
|
|
89
89
|
- No CONTRIBUTING parsing (ADR-0007)
|
|
90
90
|
- No private-repo support
|
|
91
|
-
- No `Worker` orchestrator class —
|
|
91
|
+
- No `Worker` orchestrator class — `fix` is a state machine in `Update`
|
|
92
92
|
|
|
93
93
|
**Stop and check in with Chris.** This is the demo for the workshop opening.
|
|
94
94
|
|
|
@@ -118,7 +118,7 @@ Minimum viable workshop = Stages 1, 2, and 4 done. Stage 3 (the TUI) can become
|
|
|
118
118
|
- "Authenticated as @user" indicator
|
|
119
119
|
- Sort gems by issue count
|
|
120
120
|
- Skip path/git source gems with a clear status line
|
|
121
|
-
- Confirmation dialog before
|
|
121
|
+
- Confirmation dialog before `fix`
|
|
122
122
|
- `o` to open the gem's homepage in browser
|
|
123
123
|
- "Last updated" warning for stale-looking gems
|
|
124
124
|
|
|
@@ -147,7 +147,7 @@ You're ready when, on a fresh laptop:
|
|
|
147
147
|
|
|
148
148
|
1. `git clone … && cd gem-contribute && bundle install` succeeds
|
|
149
149
|
2. `bin/gem-contribute` launches the TUI against a real `Gemfile.lock`
|
|
150
|
-
3. `f` on an issue completes the
|
|
150
|
+
3. `f` on an issue completes the `fix` flow with the device-flow prompt firing
|
|
151
151
|
4. The workshop issues are visible on the public GitHub repo with the `workshop` label
|
|
152
152
|
5. `docs/workshop.md` reads cleanly to someone who hasn't seen the project
|
|
153
153
|
|
data/docs/talk/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Talk source
|
|
2
|
+
|
|
3
|
+
Lightning talk for Blue Ridge Ruby 2026: *Building what you cannot find*.
|
|
4
|
+
|
|
5
|
+
`lightning.md` is a [Marp](https://marp.app/) presentation. The same file
|
|
6
|
+
is the source of truth for HTML, PDF, and the slides shown live.
|
|
7
|
+
|
|
8
|
+
## Render the slides
|
|
9
|
+
|
|
10
|
+
The easiest path is the Marp VS Code extension — open `lightning.md` and
|
|
11
|
+
preview it in the side panel. For a finished export:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
# one-time
|
|
15
|
+
npm install -g @marp-team/marp-cli
|
|
16
|
+
|
|
17
|
+
# preview live in browser
|
|
18
|
+
marp --server talk/
|
|
19
|
+
|
|
20
|
+
# publish to GitHub Pages (committed; appears at /talk/ on the docs site)
|
|
21
|
+
marp talk/lightning.md --html -o docs/talk/index.html
|
|
22
|
+
|
|
23
|
+
# export to PDF (recommended for the talk itself — survives wifi)
|
|
24
|
+
marp talk/lightning.md --pdf -o talk/lightning.pdf
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The HTML version under `docs/talk/index.html` is served via the docs
|
|
28
|
+
site at <https://cdhagmann.com/gem-contribute/talk/>. Re-run the `--html`
|
|
29
|
+
command and commit the changed file whenever the slides change.
|
|
30
|
+
|
|
31
|
+
## Speaker notes
|
|
32
|
+
|
|
33
|
+
Embedded as `<!-- ... -->` HTML comments inside `lightning.md`. They
|
|
34
|
+
don't render in the slide output but show up in Marp's presenter view.
|
|
35
|
+
|
|
36
|
+
## Demo recording
|
|
37
|
+
|
|
38
|
+
Always record the live demo before the talk and have the video queued
|
|
39
|
+
on the second monitor. Wifi at conferences is unreliable. If `submit`
|
|
40
|
+
hangs, you swap to the recording without losing the audience.
|
|
41
|
+
|
|
42
|
+
## Timing
|
|
43
|
+
|
|
44
|
+
Target 5 minutes. Practice with a phone timer at least four times
|
|
45
|
+
standing up. Cut on every pass.
|