opencode-ruby 0.0.1.alpha7 → 0.0.1.alpha9
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/CHANGELOG.md +35 -1
- data/README.md +37 -9
- data/examples/conversation_recipe.rb +3 -2
- data/lib/opencode/client.rb +68 -31
- data/lib/opencode/version.rb +1 -1
- data/lib/opencode-ruby.rb +2 -0
- data/opencode-ruby.gemspec +3 -7
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d0723db2b0d77a65c6718f0dcc49b67d298a62c33996169a697eb47c9436980a
|
|
4
|
+
data.tar.gz: c0446dbb4539a3432e6d0b71f4790b6c16377ac8aad32406c6f178b04c1dbf54
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 73205b52ce2f279b8daee9f7274f52d3ff2382cba757f8af2cfce0d2addb0e67c1bf7b684f6575b9161af53678bccfd0ff40c154afe2336b4202fff5f2085f7a
|
|
7
|
+
data.tar.gz: 98d7e60949dcad273ab8533c4652902084f0df117cb52ec522ee7a9cdf8bf35906093479634350b395ea1159d9d881e1b683c70a53fd7be5f5815d4d96cb0d84
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.1.alpha9 - 2026-07-20
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Parse SSE events with CRLF, CR-only, or LF framing; accept `data:` fields
|
|
8
|
+
with or without the optional leading space; join multiple `data:` fields;
|
|
9
|
+
ignore the stream's optional leading UTF-8 BOM and comment fields; and decode
|
|
10
|
+
correctly when a transport chunk splits any byte of the event framing.
|
|
11
|
+
- Keep request instrumentation free of query credentials and private workspace
|
|
12
|
+
paths.
|
|
13
|
+
- Load non-empty todo events and written-file artifacts in standalone Ruby
|
|
14
|
+
clients.
|
|
15
|
+
- Bound reconnects after an interactive prompt loses its event stream, handle
|
|
16
|
+
DNS failures as transient stream errors, and map non-404 4xx responses to
|
|
17
|
+
`BadRequestError` consistently.
|
|
18
|
+
- Retry the Rails recovery recipe with its replacement session and keep
|
|
19
|
+
exception details out of user-facing message content.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Test Ruby 3.2, 3.3, 3.4, and 4.0; pin every third-party CI and release
|
|
24
|
+
action to an exact reviewed commit; and use Ruby 4.0 for release builds.
|
|
25
|
+
- Fail the trusted-publishing job before release when the pushed tag does not
|
|
26
|
+
match `Opencode::VERSION`.
|
|
27
|
+
|
|
28
|
+
## 0.0.1.alpha8 - 2026-07-20
|
|
29
|
+
|
|
30
|
+
### Yanked
|
|
31
|
+
|
|
32
|
+
- Published from the unrepaired alpha8 source by mistake and yanked the same
|
|
33
|
+
day. Use alpha9, which contains the intended alpha8 fixes and release gates.
|
|
34
|
+
|
|
3
35
|
## 0.0.1.alpha7 - 2026-07-18
|
|
4
36
|
|
|
5
37
|
### Fixed
|
|
@@ -100,5 +132,7 @@ First public alpha. HTTP + SSE client for OpenCode REST API.
|
|
|
100
132
|
### Compatibility
|
|
101
133
|
|
|
102
134
|
- Ruby ≥ 3.2
|
|
103
|
-
- OpenCode
|
|
135
|
+
- OpenCode targeted the then-current 1.15 message-bus shape. This historical
|
|
136
|
+
target was not a blanket SemVer compatibility guarantee; use the README's
|
|
137
|
+
current compatibility evidence for deployment decisions.
|
|
104
138
|
- Runtime dependency: `activesupport (>= 6.1)` for `blank?`/`present?`/`presence`/`truncate`/`duplicable?`/`megabytes`. ActiveSupport is *not* Rails — it's a standalone helpers gem.
|
data/README.md
CHANGED
|
@@ -67,7 +67,9 @@ child = client.create_session(
|
|
|
67
67
|
|
|
68
68
|
Model strings use OpenCode's `provider/model` form; a preformatted model hash
|
|
69
69
|
with `providerID` and `id` keys is also accepted. These configured-session
|
|
70
|
-
fields
|
|
70
|
+
fields first appeared in OpenCode 1.16.1. That is an API-introduction note,
|
|
71
|
+
not evidence that every later server is compatible; use the
|
|
72
|
+
[certified compatibility evidence](#compatibility) for deployment choices.
|
|
71
73
|
|
|
72
74
|
### Streaming (the headline)
|
|
73
75
|
|
|
@@ -106,8 +108,9 @@ client.update_session(session_id, permissions: permission_rules)
|
|
|
106
108
|
|
|
107
109
|
OpenCode appends PATCHed permission rules and evaluates the last matching
|
|
108
110
|
rule. Hosts should send a complete ordered policy and fingerprint it so the
|
|
109
|
-
same policy is not appended on every turn. This endpoint
|
|
110
|
-
1.16.1
|
|
111
|
+
same policy is not appended on every turn. This endpoint first appeared in
|
|
112
|
+
OpenCode 1.16.1; that version floor describes endpoint availability, not
|
|
113
|
+
compatibility with every later server.
|
|
111
114
|
|
|
112
115
|
### Lower-level event firehose
|
|
113
116
|
|
|
@@ -199,8 +202,29 @@ Want every OpenCode endpoint auto-generated from the OpenAPI spec? Use [`opencod
|
|
|
199
202
|
## Compatibility
|
|
200
203
|
|
|
201
204
|
- Ruby ≥ 3.2
|
|
202
|
-
-
|
|
203
|
-
- Runtime dependency: `
|
|
205
|
+
- Runtime dependency: `activesupport (>= 6.1, < 9.0)` — *not* Rails. ActiveSupport is a standalone helpers gem (`blank?`, `present?`, `presence`, `truncate`, etc.).
|
|
206
|
+
- Runtime dependency: `marcel (~> 1.0)` for artifact MIME type detection.
|
|
207
|
+
|
|
208
|
+
OpenCode server compatibility is evidence-based, not an open-ended SemVer
|
|
209
|
+
promise. OpenCode's HTTP, SSE, and runtime behavior can change independently
|
|
210
|
+
of a numeric version floor, so this gem does not infer compatibility from a
|
|
211
|
+
constraint such as `>= 1.15`.
|
|
212
|
+
|
|
213
|
+
Use the [OpenCode compatibility corpus](https://github.com/ajaynomics/opencode-compat)
|
|
214
|
+
as the source of current evidence. Its
|
|
215
|
+
[image matrix](https://github.com/ajaynomics/opencode-compat/blob/main/manifests/image-matrix.json)
|
|
216
|
+
records the exact gem commit and immutable OpenCode OCI digest exercised by
|
|
217
|
+
the `ruby-rest-sse` profile. Its
|
|
218
|
+
[runtime tuples](https://github.com/ajaynomics/opencode-compat/blob/main/manifests/runtime-tuples.json)
|
|
219
|
+
add the exact consumer commit and canary evidence needed for promotion and
|
|
220
|
+
rollback. A version label or image tag is provenance only, never the execution
|
|
221
|
+
coordinate.
|
|
222
|
+
|
|
223
|
+
A combination absent from passing evidence is **unverified**, not necessarily
|
|
224
|
+
incompatible. Run the corpus against the exact gem, image digest, and consumer
|
|
225
|
+
commit before adopting or promoting it. See the
|
|
226
|
+
[certification policy](https://github.com/ajaynomics/opencode-compat/blob/main/docs/certification.md)
|
|
227
|
+
for the full process.
|
|
204
228
|
|
|
205
229
|
## Development
|
|
206
230
|
|
|
@@ -213,10 +237,14 @@ The smoke suite covers Client end-to-end against WebMock-stubbed OpenCode
|
|
|
213
237
|
endpoints, including subscription-before-prompt ordering and
|
|
214
238
|
reconnect-without-repost.
|
|
215
239
|
|
|
216
|
-
|
|
217
|
-
`
|
|
218
|
-
|
|
219
|
-
|
|
240
|
+
The mistakenly published `0.0.1.alpha8` package contained unrepaired source and
|
|
241
|
+
was yanked; use `0.0.1.alpha9` or later. The repository contains a tag-triggered
|
|
242
|
+
`release.yml` workflow intended for RubyGems trusted publishing, but its
|
|
243
|
+
trusted-publisher registration is not confirmed for alpha9. A `v*` tag push
|
|
244
|
+
therefore does not currently guarantee publication. Before a future release,
|
|
245
|
+
verify the RubyGems registry result explicitly. Once the workflow is registered
|
|
246
|
+
as a trusted publisher for the `release` environment, it can build, attest, and
|
|
247
|
+
publish without a long-lived RubyGems API key.
|
|
220
248
|
|
|
221
249
|
## License
|
|
222
250
|
|
|
@@ -128,12 +128,13 @@ class GenerateAssistantReplyJob < ApplicationJob
|
|
|
128
128
|
end
|
|
129
129
|
rescue Opencode::SessionNotFoundError, Opencode::StaleSessionError
|
|
130
130
|
raise if attempted_recreate
|
|
131
|
-
message.conversation.recreate_opencode_session!(client)
|
|
131
|
+
session_id = message.conversation.recreate_opencode_session!(client)
|
|
132
132
|
attempted_recreate = true
|
|
133
133
|
retry
|
|
134
134
|
end
|
|
135
135
|
rescue StandardError => e
|
|
136
|
-
|
|
136
|
+
Rails.logger.error(e.full_message)
|
|
137
|
+
message&.update!(status: :errored, content: "An error occurred. Please try again.")
|
|
137
138
|
end
|
|
138
139
|
|
|
139
140
|
private
|
data/lib/opencode/client.rb
CHANGED
|
@@ -230,18 +230,12 @@ module Opencode
|
|
|
230
230
|
request = Net::HTTP::Get.new(uri)
|
|
231
231
|
add_auth_header(request)
|
|
232
232
|
|
|
233
|
-
response = Opencode::Instrumentation.instrument("opencode.request", method: request.method, path:
|
|
233
|
+
response = Opencode::Instrumentation.instrument("opencode.request", method: request.method, path: uri.path) do
|
|
234
234
|
http_client.request(request)
|
|
235
235
|
end
|
|
236
236
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
return [] if response.body.blank?
|
|
242
|
-
JSON.parse(response.body, symbolize_names: true)
|
|
243
|
-
rescue JSON::ParserError => e
|
|
244
|
-
raise ServerError, "list_questions returned invalid JSON: #{e.message}"
|
|
237
|
+
body = handle_response(response)
|
|
238
|
+
body.is_a?(Array) ? body : []
|
|
245
239
|
rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout => e
|
|
246
240
|
raise TimeoutError, "OpenCode timeout after #{@timeout}s: #{e.message}"
|
|
247
241
|
rescue Errno::ECONNREFUSED, SocketError => e
|
|
@@ -255,6 +249,10 @@ module Opencode
|
|
|
255
249
|
end
|
|
256
250
|
|
|
257
251
|
MAX_SSE_BUFFER = 1_048_576 # 1 MB — safety valve against pathological server responses
|
|
252
|
+
# Keep CRLF atomic so it cannot backtrack into CR + LF and look like a
|
|
253
|
+
# blank line. SSE also permits bare CR and LF line endings.
|
|
254
|
+
SSE_EVENT_BOUNDARY = /(?>\r\n|[\r\n]){2}/
|
|
255
|
+
UTF8_BOM = "\xEF\xBB\xBF".b.freeze
|
|
258
256
|
SSE_RECONNECT_DELAY = 0.1
|
|
259
257
|
TRANSIENT_SSE_ERRORS = [
|
|
260
258
|
EOFError,
|
|
@@ -263,7 +261,8 @@ module Opencode
|
|
|
263
261
|
Net::ReadTimeout,
|
|
264
262
|
Errno::ECONNREFUSED,
|
|
265
263
|
Errno::ECONNRESET,
|
|
266
|
-
Errno::EPIPE
|
|
264
|
+
Errno::EPIPE,
|
|
265
|
+
SocketError
|
|
267
266
|
].freeze
|
|
268
267
|
|
|
269
268
|
# Opens SSE connection to GET /event, yields parsed events filtered by session_id.
|
|
@@ -330,7 +329,7 @@ module Opencode
|
|
|
330
329
|
|
|
331
330
|
loop do
|
|
332
331
|
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
333
|
-
deadline =
|
|
332
|
+
deadline = check_deadline(now, deadline, timeout)
|
|
334
333
|
|
|
335
334
|
# NOTE: first_event_deadline is *not* suspension-eligible. If the agent
|
|
336
335
|
# never gets started we want to fail fast — a session that's blocked on
|
|
@@ -357,9 +356,11 @@ module Opencode
|
|
|
357
356
|
http.read_timeout = 30
|
|
358
357
|
|
|
359
358
|
subscription_callback_error = nil
|
|
359
|
+
event_callback_error = nil
|
|
360
360
|
subscription_ready = on_subscribed.nil?
|
|
361
361
|
begin
|
|
362
362
|
buffer = String.new
|
|
363
|
+
first_sse_event = true
|
|
363
364
|
|
|
364
365
|
http.request(request) do |response|
|
|
365
366
|
unless response.is_a?(Net::HTTPSuccess)
|
|
@@ -386,9 +387,14 @@ module Opencode
|
|
|
386
387
|
raise ServerError, "SSE buffer exceeded #{MAX_SSE_BUFFER} bytes"
|
|
387
388
|
end
|
|
388
389
|
|
|
389
|
-
while (
|
|
390
|
-
raw_event = buffer.slice!(0,
|
|
391
|
-
event = parse_sse_event(
|
|
390
|
+
while (boundary = buffer.match(SSE_EVENT_BOUNDARY))
|
|
391
|
+
raw_event = buffer.slice!(0, boundary.end(0))
|
|
392
|
+
event = parse_sse_event(
|
|
393
|
+
raw_event,
|
|
394
|
+
session_id,
|
|
395
|
+
allow_bom: first_sse_event
|
|
396
|
+
)
|
|
397
|
+
first_sse_event = false
|
|
392
398
|
next unless event
|
|
393
399
|
|
|
394
400
|
unless subscription_ready
|
|
@@ -438,14 +444,19 @@ module Opencode
|
|
|
438
444
|
# that's the whole point: a healthy long wait (user thinking
|
|
439
445
|
# for 30 minutes) keeps the container warm via heartbeats so
|
|
440
446
|
# the reaper doesn't kill it mid-wait.
|
|
441
|
-
|
|
442
|
-
|
|
447
|
+
begin
|
|
448
|
+
on_activity_tick&.call(event)
|
|
449
|
+
block.call(event)
|
|
450
|
+
rescue StandardError => error
|
|
451
|
+
event_callback_error = error
|
|
452
|
+
raise
|
|
453
|
+
end
|
|
443
454
|
return if terminal_session_event?(event)
|
|
444
455
|
end
|
|
445
456
|
end
|
|
446
457
|
end
|
|
447
458
|
rescue *TRANSIENT_SSE_ERRORS
|
|
448
|
-
raise if subscription_callback_error
|
|
459
|
+
raise if subscription_callback_error || event_callback_error
|
|
449
460
|
|
|
450
461
|
# Treat transport-level SSE disconnects like clean EOF: reconnect
|
|
451
462
|
# until an idle session event, the overall timeout, or first-event
|
|
@@ -504,6 +515,11 @@ module Opencode
|
|
|
504
515
|
# the prompt resolves. Otherwise apply the normal deadline check.
|
|
505
516
|
def check_deadline_or_suspend(now, deadline, timeout, reply)
|
|
506
517
|
return now + timeout if reply&.prompt_blocked?
|
|
518
|
+
|
|
519
|
+
check_deadline(now, deadline, timeout)
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
def check_deadline(now, deadline, timeout)
|
|
507
523
|
raise TimeoutError, "SSE stream timed out after #{timeout}s" if now > deadline
|
|
508
524
|
|
|
509
525
|
deadline
|
|
@@ -618,7 +634,7 @@ module Opencode
|
|
|
618
634
|
add_auth_header(request)
|
|
619
635
|
|
|
620
636
|
response = nil
|
|
621
|
-
result = Opencode::Instrumentation.instrument("opencode.request", method: request.method, path: request.path) do
|
|
637
|
+
result = Opencode::Instrumentation.instrument("opencode.request", method: request.method, path: request.uri.path) do
|
|
622
638
|
response = http_client.request(request)
|
|
623
639
|
handle_response(response)
|
|
624
640
|
end
|
|
@@ -639,11 +655,22 @@ module Opencode
|
|
|
639
655
|
end
|
|
640
656
|
end
|
|
641
657
|
|
|
642
|
-
def parse_sse_event(raw, session_id)
|
|
643
|
-
|
|
644
|
-
|
|
658
|
+
def parse_sse_event(raw, session_id, allow_bom: false)
|
|
659
|
+
if allow_bom && raw.b.start_with?(UTF8_BOM)
|
|
660
|
+
raw = raw.byteslice(UTF8_BOM.bytesize..)
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
data = raw.split(/\r\n|\r|\n/, -1).filter_map do |line|
|
|
664
|
+
next if line.start_with?(":")
|
|
665
|
+
|
|
666
|
+
field, value = line.split(":", 2)
|
|
667
|
+
next unless field == "data"
|
|
645
668
|
|
|
646
|
-
|
|
669
|
+
(value || "").delete_prefix(" ")
|
|
670
|
+
end
|
|
671
|
+
return nil if data.empty?
|
|
672
|
+
|
|
673
|
+
json = JSON.parse(data.join("\n"), symbolize_names: true)
|
|
647
674
|
|
|
648
675
|
event_session = json.dig(:properties, :sessionID) ||
|
|
649
676
|
json.dig(:properties, :info, :sessionID) ||
|
|
@@ -659,23 +686,30 @@ module Opencode
|
|
|
659
686
|
end
|
|
660
687
|
|
|
661
688
|
def handle_response(response)
|
|
662
|
-
|
|
689
|
+
status = response.code.to_i
|
|
690
|
+
return {} if status == 204
|
|
663
691
|
|
|
664
|
-
body =
|
|
665
|
-
JSON.parse(response.body, symbolize_names: true)
|
|
666
|
-
else
|
|
667
|
-
{}
|
|
668
|
-
end
|
|
692
|
+
body = parse_response_body(response, status)
|
|
669
693
|
|
|
670
|
-
case
|
|
694
|
+
case status
|
|
671
695
|
when 200..299 then body
|
|
672
|
-
when 400 then raise BadRequestError.new(error_message(body, "Bad request"), response: body)
|
|
673
696
|
when 404 then raise SessionNotFoundError.new(error_message(body, "Session not found"), response: body)
|
|
697
|
+
when 400..499 then raise BadRequestError.new(error_message(body, "Bad request"), response: body)
|
|
674
698
|
when 500..599 then raise ServerError.new(error_message(body, "Server error"), response: body)
|
|
675
699
|
else raise Error.new("Unexpected response: #{response.code}", response: body)
|
|
676
700
|
end
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
def parse_response_body(response, status)
|
|
704
|
+
return {} if response.body.blank?
|
|
705
|
+
|
|
706
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
677
707
|
rescue JSON::ParserError
|
|
678
|
-
|
|
708
|
+
if status.between?(200, 299)
|
|
709
|
+
raise ServerError.new("Invalid JSON from OpenCode (HTTP #{response.code}): #{response.body&.truncate(200)}")
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
{ message: response.body.to_s.truncate(200) }
|
|
679
713
|
end
|
|
680
714
|
|
|
681
715
|
# OpenCode HTTP error bodies use a wrapped shape: { name:, data: { message:, kind?: } }.
|
|
@@ -683,6 +717,9 @@ module Opencode
|
|
|
683
717
|
# `body[:message]` is no longer populated for errors — only `body[:data][:message]`.
|
|
684
718
|
# We read both to keep older mock servers working in tests.
|
|
685
719
|
def error_message(body, fallback)
|
|
720
|
+
return body.truncate(200) if body.is_a?(String) && body.present?
|
|
721
|
+
return fallback unless body.is_a?(Hash)
|
|
722
|
+
|
|
686
723
|
body.dig(:data, :message) || body[:message] || fallback
|
|
687
724
|
end
|
|
688
725
|
end
|
data/lib/opencode/version.rb
CHANGED
data/lib/opencode-ruby.rb
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
# non-Rails apps.
|
|
7
7
|
require "active_support/core_ext/object/blank" # provides blank?, present?, presence
|
|
8
8
|
require "active_support/core_ext/object/duplicable"
|
|
9
|
+
require "active_support/core_ext/hash/keys" # provides Hash#deep_stringify_keys
|
|
9
10
|
require "active_support/core_ext/string/filters" # provides String#truncate
|
|
10
11
|
require "active_support/core_ext/numeric/bytes" # provides Integer#megabytes
|
|
12
|
+
require "marcel"
|
|
11
13
|
|
|
12
14
|
require_relative "opencode/version"
|
|
13
15
|
require_relative "opencode/error"
|
data/opencode-ruby.gemspec
CHANGED
|
@@ -30,14 +30,10 @@ Gem::Specification.new do |spec|
|
|
|
30
30
|
%w[README.md LICENSE CHANGELOG.md opencode-ruby.gemspec]
|
|
31
31
|
spec.require_paths = ["lib"]
|
|
32
32
|
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
# `truncate`/`duplicable?` helpers used in this gem's code. It does NOT
|
|
36
|
-
# pull in ActiveRecord, ActionView, ActionController, Turbo, or any other
|
|
37
|
-
# Rails-only piece. Most Ruby apps in the wild already have ActiveSupport
|
|
38
|
-
# transitively via another gem; in the rare case yours doesn't, ~250 LOC
|
|
39
|
-
# of core_ext is added when this gem installs.
|
|
33
|
+
# ActiveSupport supplies the small set of core extensions used by the
|
|
34
|
+
# client without pulling in Rails. Marcel identifies artifact content types.
|
|
40
35
|
spec.add_runtime_dependency "activesupport", ">= 6.1", "< 9.0"
|
|
36
|
+
spec.add_runtime_dependency "marcel", "~> 1.0"
|
|
41
37
|
|
|
42
38
|
spec.add_development_dependency "minitest", "~> 5.20"
|
|
43
39
|
spec.add_development_dependency "rake", "~> 13.0"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: opencode-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.1.
|
|
4
|
+
version: 0.0.1.alpha9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ajay Krishnan
|
|
@@ -29,6 +29,20 @@ dependencies:
|
|
|
29
29
|
- - "<"
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
31
|
version: '9.0'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: marcel
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - "~>"
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '1.0'
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - "~>"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '1.0'
|
|
32
46
|
- !ruby/object:Gem::Dependency
|
|
33
47
|
name: minitest
|
|
34
48
|
requirement: !ruby/object:Gem::Requirement
|