studio-engine 0.76.3 → 0.77.1
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 +34 -0
- data/README.md +18 -0
- data/app/views/layouts/_navbar.html.erb +49 -7
- data/app/views/navbar/show.html.erb +25 -8
- data/lib/studio/s3.rb +47 -2
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +24 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e8cc0d34a62b18829e9caa043f820a5dc6100fa9de2d990f32dd4f3439c5bd40
|
|
4
|
+
data.tar.gz: b5a7794c1c53bea3354f4b48e46887c9315df5fbff6d671436328fac9f95c2d3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 65d91ace8c12010d262fb841a19c384ef85aa8418d6ec270cf30d2b68e28f73f1da5cdbfe9057cdefad831e44c519ea088468f36ce3a9467866102da47d08491
|
|
7
|
+
data.tar.gz: e56bd88f0517331aeb58ddac29ab027053a74189a97c78c3a55b9b8cd4f8827fa3492676b4604c0e779f96d7136073b11e8e4e624ed7e064654c037c676457c0
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,40 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.77.1 — 2026-09-27
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **The engine header fits a phone.** `.user-nav-col` / `.user-nav-fit` capped their
|
|
12
|
+
two phone bands at a constant `14rem`/`15rem` — 224px is 57% of a 390px screen — on
|
|
13
|
+
a `flex-shrink-0` column, leaving the left column 166px for a 48px logo, its gap,
|
|
14
|
+
32px of padding and the app's name. Every flex item defaults to `min-width: auto`,
|
|
15
|
+
so the name could not truncate either: it drew across the gap and on top of the user
|
|
16
|
+
column beside it. The bands are now `min(14rem, 46vw)` and `min(15rem, 46vw)`, with
|
|
17
|
+
`min-w-0` on each flex item between the nav row and the title and `truncate` on the
|
|
18
|
+
two title spans. `min()` resolves to the plain rem at ~487px and ~522px, so the
|
|
19
|
+
768px band and every desktop width are unchanged; below ~344px the app name
|
|
20
|
+
truncates rather than spilling.
|
|
21
|
+
- **Consumers that render `layouts/_navbar` unforked inherit this on upgrade** —
|
|
22
|
+
`mcritchie-industries` and `acquisition-studio`. `mcritchie-studio` inlines its own
|
|
23
|
+
`<header>` and `turf-monster` forks the partial and declares its own
|
|
24
|
+
`.user-nav-col` widths, so each adopts the change separately.
|
|
25
|
+
|
|
26
|
+
## 0.77.0 — 2026-09-26
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **`Studio::S3` runs on Cloudflare R2, or any S3-compatible endpoint.** Four new
|
|
31
|
+
settings, all nil by default: `s3_endpoint`, `s3_access_key_id` +
|
|
32
|
+
`s3_secret_access_key` (passed to the client as a pair, so an app can hold its
|
|
33
|
+
R2 keys beside the `AWS_*` ones during a cutover; half a pair raises), and
|
|
34
|
+
`s3_public_url`, the base public objects are served from. Unset, every app gets
|
|
35
|
+
the same client and the same amazonaws.com URLs as before. On an endpoint with
|
|
36
|
+
no public base, `url` raises `NotConfigured` instead of building a URL R2 would
|
|
37
|
+
refuse, and `upload` still writes but returns `nil`. Proven live against an R2
|
|
38
|
+
bucket: upload, download, exists, list, signed URL (200) and delete, with the
|
|
39
|
+
anonymous endpoint read refused.
|
|
40
|
+
|
|
7
41
|
## 0.76.3 — 2026-09-23
|
|
8
42
|
|
|
9
43
|
### Fixed
|
data/README.md
CHANGED
|
@@ -838,6 +838,24 @@ config.s3_key_prefix = "mcritchie-industries/"
|
|
|
838
838
|
logical keys and never see it. Unset (the default) leaves keys byte-identical to
|
|
839
839
|
what every already-shipped app wrote.
|
|
840
840
|
|
|
841
|
+
### S3-compatible storage (Cloudflare R2)
|
|
842
|
+
|
|
843
|
+
`Studio::S3` talks to any S3-compatible endpoint. The McRitchie fleet is moving
|
|
844
|
+
to R2 (one account, McRitchie Studio's); an app switches in its initializer:
|
|
845
|
+
|
|
846
|
+
```ruby
|
|
847
|
+
config.s3_endpoint = ENV["R2_ENDPOINT"] # https://<account>.r2.cloudflarestorage.com
|
|
848
|
+
config.s3_region = "auto"
|
|
849
|
+
config.s3_access_key_id = ENV["R2_ACCESS_KEY_ID"]
|
|
850
|
+
config.s3_secret_access_key = ENV["R2_SECRET_ACCESS_KEY"]
|
|
851
|
+
config.s3_public_url = ENV["R2_PUBLIC_URL"] # custom domain on this env's bucket
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
All four are nil by default, and nil is AWS exactly as before. The keys must be
|
|
855
|
+
set as a pair. R2 serves nothing anonymously from its S3 endpoint, so with an
|
|
856
|
+
endpoint and no `s3_public_url`, `Studio::S3.url` raises `NotConfigured` and
|
|
857
|
+
`upload` writes but returns `nil`; serve private objects with `signed_url`.
|
|
858
|
+
|
|
841
859
|
An app with **no** bucket configured does not error — `/admin/emails` renders
|
|
842
860
|
read-only, showing the inherited defaults it is genuinely sending and naming the
|
|
843
861
|
one setting that turns uploads on.
|
|
@@ -62,13 +62,44 @@
|
|
|
62
62
|
class="nav-shell <%= is_preview ? 'bg-page' : "#{'vt-pinned-header ' if pin_header}sticky top-0 z-50 bg-page transition-shadow duration-300" %>"
|
|
63
63
|
:class="scrolled && 'shadow-lg border-b border-subtle is-scrolled'">
|
|
64
64
|
<style>
|
|
65
|
-
|
|
65
|
+
/* THE PHONE STEP IS A SHARE OF THE VIEWPORT, NOT A CONSTANT, and that is the
|
|
66
|
+
whole width fix. 14rem is 224px — 57% of a 390px screen — handed to a
|
|
67
|
+
flex-shrink-0 column, so the left column was left 166px for a 48px logo,
|
|
68
|
+
a 4px gap, 32px of padding and whatever the app is called. MEASURED on
|
|
69
|
+
/lab/bar_stack?signed_in=1 at 390x844, app_name "McRitchie Industries":
|
|
70
|
+
the title's own box ran to x=175.3 inside a column whose border box ends
|
|
71
|
+
at x=166, and "Industries" painted ON TOP of the wallet address in the
|
|
72
|
+
column beside it. documentElement.scrollWidth was 390 against a
|
|
73
|
+
clientWidth of 390 the entire time — the page did not scroll sideways,
|
|
74
|
+
so every document-level check of this header was green while the header
|
|
75
|
+
was visibly broken.
|
|
76
|
+
|
|
77
|
+
min() rather than a new breakpoint: the cap still resolves to 14rem on
|
|
78
|
+
anything at or above ~487px, so this band's desktop-side behaviour is
|
|
79
|
+
byte-identical and only the phone widths move. 46vw is not a taste
|
|
80
|
+
value — it is the largest share that still leaves the left column the
|
|
81
|
+
191px a 48px logo, its gap, its padding and a ten-character word need at
|
|
82
|
+
360px, the narrowest mainstream Android. Below that the title truncates
|
|
83
|
+
(see min-w-0 + truncate on the markup) instead of spilling.
|
|
84
|
+
|
|
85
|
+
BOTH LINES, and that is not redundancy. .user-nav-col is a hard width
|
|
86
|
+
(a host passing balance_html reserves the seat) and .user-nav-fit is a
|
|
87
|
+
cap (no balance, so the column hugs its content). They are two different
|
|
88
|
+
properties and a fix to one leaves the other exactly as broken. */
|
|
89
|
+
.user-nav-col { width: min(14rem, 46vw); }
|
|
66
90
|
/* No balance content → no reserved column. The nav hugs its icons and
|
|
67
91
|
name instead of floating in the balance's empty seat. max-width steps
|
|
68
92
|
mirror the col's clamps: the item is flex-shrink-0, so without them a
|
|
69
93
|
long nowrap username sizes the column past a narrow viewport. */
|
|
70
|
-
.user-nav-fit { max-width: 14rem; }
|
|
71
|
-
|
|
94
|
+
.user-nav-fit { max-width: min(14rem, 46vw); }
|
|
95
|
+
/* THE SAME SHARE IN THE NEXT BAND UP, and leaving it out was measurably
|
|
96
|
+
worse than not fixing the first one. With only the base band capped, a
|
|
97
|
+
412px screen (Pixel-class, and the 400px band's own territory) gave the
|
|
98
|
+
title 84px while a 390px screen gave it 107 — a WIDER phone showing LESS
|
|
99
|
+
of the app's name. Monotone or it is not fixed. min(15rem, 46vw) resolves
|
|
100
|
+
to the plain 15rem at ~522px and up, so everything above this band's
|
|
101
|
+
phone edge is unchanged. */
|
|
102
|
+
@media (min-width: 400px) { .user-nav-col { width: min(15rem, 46vw); } .user-nav-fit { max-width: min(15rem, 46vw); } }
|
|
72
103
|
@media (min-width: 768px) { .user-nav-col { width: 20rem; } .user-nav-fit { max-width: 20rem; } }
|
|
73
104
|
/* The COLLAPSE band table used to live here, which meant only an app
|
|
74
105
|
rendering THIS partial got it. Three of five apps fork the navbar, so each
|
|
@@ -88,16 +119,27 @@
|
|
|
88
119
|
</style>
|
|
89
120
|
<div class="nav-row flex items-center">
|
|
90
121
|
<%# Left side: logo + nav %>
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
122
|
+
<%# min-w-0 THREE TIMES, on every flex item between the row and the title.
|
|
123
|
+
A flex item's default min-width is auto, which floors it at its own
|
|
124
|
+
min-content — so ONE of these left out puts the floor back and the
|
|
125
|
+
title spills across the gap again. The chain is: row item (.flex-1) ->
|
|
126
|
+
its inner flex -> the logo link. The <h1> is the fourth link and carries
|
|
127
|
+
its own min-w-0 below. %>
|
|
128
|
+
<div class="flex-1 min-w-0 px-4">
|
|
129
|
+
<div class="flex items-center gap-6 min-w-0">
|
|
130
|
+
<%= link_to root_path, class: "nav-logo-link inline-flex items-center gap-3 group min-w-0" do %>
|
|
94
131
|
<% if logo_path %>
|
|
95
132
|
<%# No w-*/h-* utility here: .nav-shell .nav-logo sizes it off --nav-p.
|
|
96
133
|
transition-transform, NOT transition-all — the hover lift still
|
|
97
134
|
eases, while width/height follow the finger with no clock. %>
|
|
98
135
|
<img src="<%= logo_path %>" alt="<%= Studio.app_name %>" class="nav-logo rounded-full shadow-lg <%= "group-hover:scale-105 transition-transform duration-300" unless is_preview %>">
|
|
99
136
|
<% end %>
|
|
100
|
-
|
|
137
|
+
<%# truncate on the SPANS, not the h1. .nav-title is itself a flex
|
|
138
|
+
container (a column below 768px, a baseline row above), so the
|
|
139
|
+
overflow that has to be clipped belongs to each word's own box —
|
|
140
|
+
`truncate` on the h1 would clip the flex CONTAINER and leave the
|
|
141
|
+
spans drawing outside it, which is the bug rather than the fix. %>
|
|
142
|
+
<h1 class="nav-title font-extrabold text-heading tracking-tight min-w-0"><span class="truncate"><%= prefix %></span><span class="text-primary truncate"><%= last_word %></span></h1>
|
|
101
143
|
<% end %>
|
|
102
144
|
<%# Desktop nav — apps should override this partial to add links here %>
|
|
103
145
|
<nav class="hidden md:flex items-center gap-4">
|
|
@@ -41,21 +41,38 @@
|
|
|
41
41
|
--nav-title-lead-size: calc(1.5rem - 0.35rem * var(--nav-p));
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/* THE USER-COLUMN WIDTH STEPS, re-keyed on the wrapper for the same reason
|
|
45
|
+
the --nav-p sizes above are: the shipped rules live in media queries, which
|
|
46
|
+
cannot fire for a CONTAINER at a fixed viewport width.
|
|
47
|
+
|
|
48
|
+
PERCENT HERE, vw THERE, AND THEY ARE THE SAME NUMBER. layouts/_navbar caps
|
|
49
|
+
the two phone bands at min(<rem>, 46vw). A literal 46vw inside a 390px
|
|
50
|
+
preview box on a 1400px monitor resolves to 644px, which would make the
|
|
51
|
+
preview show a header no phone has — so the share is expressed against the
|
|
52
|
+
CONTAINING BLOCK instead, which in this preview is the navbar row, i.e. the
|
|
53
|
+
simulated device width. Retune the cap in _navbar and retune these, or the
|
|
54
|
+
preview starts lying about the one dimension it exists to show.
|
|
55
|
+
|
|
56
|
+
REMOVED, 2026-09-27: three `.username-cap` rules, at 5/6/7rem. No element
|
|
57
|
+
in this engine or in any consuming app has ever carried that class —
|
|
58
|
+
components/_user_nav truncates the name with `truncate` and a min-w-0
|
|
59
|
+
parent instead. They were dead the day they were written and they read, to
|
|
60
|
+
the next person sizing this header, like a cap that was already in force.
|
|
61
|
+
(turf-monster's app/views/admin/navbar.html.erb carries the same three dead
|
|
62
|
+
rules; that is its own repo's to drop.) */
|
|
63
|
+
|
|
44
64
|
/* Tiny (< 400px) */
|
|
45
65
|
.navbar-preview.bp-tiny .nav-logo-link { gap: 0.25rem !important; }
|
|
46
|
-
.navbar-preview.bp-tiny .user-nav-col { width: 14rem !important; }
|
|
47
|
-
.navbar-preview.bp-tiny .user-nav-fit { max-width: 14rem !important; }
|
|
48
|
-
.navbar-preview.bp-tiny .username-cap { max-width: 5rem !important; }
|
|
66
|
+
.navbar-preview.bp-tiny .user-nav-col { width: min(14rem, 46%) !important; }
|
|
67
|
+
.navbar-preview.bp-tiny .user-nav-fit { max-width: min(14rem, 46%) !important; }
|
|
49
68
|
|
|
50
69
|
/* Small (400-767px) */
|
|
51
|
-
.navbar-preview.bp-small .user-nav-col { width: 15rem !important; }
|
|
52
|
-
.navbar-preview.bp-small .user-nav-fit { max-width: 15rem !important; }
|
|
53
|
-
.navbar-preview.bp-small .username-cap { max-width: 6rem !important; }
|
|
70
|
+
.navbar-preview.bp-small .user-nav-col { width: min(15rem, 46%) !important; }
|
|
71
|
+
.navbar-preview.bp-small .user-nav-fit { max-width: min(15rem, 46%) !important; }
|
|
54
72
|
|
|
55
|
-
/* Desktop (768px+) */
|
|
73
|
+
/* Desktop (768px+) — a bare rem, exactly as the shipped band declares it. */
|
|
56
74
|
.navbar-preview.is-desktop .user-nav-col { width: 20rem !important; }
|
|
57
75
|
.navbar-preview.is-desktop .user-nav-fit { max-width: 20rem !important; }
|
|
58
|
-
.navbar-preview.is-desktop .username-cap { max-width: 7rem !important; }
|
|
59
76
|
|
|
60
77
|
/* Scrolled state. The size half of this used to be TWELVE !important rules
|
|
61
78
|
restating every collapsed value; the toggle now just sets --nav-p: 1 on
|
data/lib/studio/s3.rb
CHANGED
|
@@ -15,17 +15,34 @@ module Studio
|
|
|
15
15
|
opts[:content_type] = content_type if content_type
|
|
16
16
|
opts[:cache_control] = cache_control if cache_control
|
|
17
17
|
client.put_object(**opts)
|
|
18
|
-
url(key: key)
|
|
18
|
+
public_url? ? url(key: key) : nil
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def download(key:)
|
|
22
22
|
client.get_object(bucket: bucket, key: full_key(key)).body.read
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# The PUBLIC URL of an object. On AWS (no endpoint) it is the bucket's
|
|
26
|
+
# virtual-hosted amazonaws.com URL, byte-identical to every version before
|
|
27
|
+
# endpoints existed. On an S3-compatible endpoint it is Studio.s3_public_url
|
|
28
|
+
# plus the key, and without that base it RAISES: R2's S3 endpoint answers no
|
|
29
|
+
# anonymous request, so any URL built from it would be a broken image in an
|
|
30
|
+
# inbox, not an error anyone sees. Private objects want signed_url.
|
|
25
31
|
def url(key:)
|
|
32
|
+
base = Studio.s3_public_url.to_s
|
|
33
|
+
return "#{base.chomp("/")}/#{full_key(key)}" unless base.empty?
|
|
34
|
+
raise NotConfigured, "Studio.s3_public_url not set: #{endpoint} serves no public URL (use signed_url for private objects)" if endpoint
|
|
35
|
+
|
|
26
36
|
"https://#{bucket}.s3.#{region}.amazonaws.com/#{full_key(key)}"
|
|
27
37
|
end
|
|
28
38
|
|
|
39
|
+
# Whether url can answer. upload asks this so an app with only private
|
|
40
|
+
# objects (a data room on R2 with no public domain) can still write:
|
|
41
|
+
# upload returns nil there instead of raising AFTER the object landed.
|
|
42
|
+
def public_url?
|
|
43
|
+
endpoint.nil? || !Studio.s3_public_url.to_s.empty?
|
|
44
|
+
end
|
|
45
|
+
|
|
29
46
|
def signed_url(key:, expires_in: 3600)
|
|
30
47
|
require "aws-sdk-s3"
|
|
31
48
|
Aws::S3::Presigner.new(client: client).presigned_url(:get_object, bucket: bucket, key: full_key(key), expires_in: expires_in)
|
|
@@ -93,11 +110,39 @@ module Studio
|
|
|
93
110
|
Studio.s3_region
|
|
94
111
|
end
|
|
95
112
|
|
|
113
|
+
# The S3-compatible endpoint (R2: https://<account>.r2.cloudflarestorage.com),
|
|
114
|
+
# or nil for AWS. Blank reads as unset, so an empty ENV var cannot point the
|
|
115
|
+
# client at "".
|
|
116
|
+
def endpoint
|
|
117
|
+
value = Studio.s3_endpoint.to_s
|
|
118
|
+
value.empty? ? nil : value
|
|
119
|
+
end
|
|
120
|
+
|
|
96
121
|
def client
|
|
97
122
|
@client ||= begin
|
|
98
123
|
require "aws-sdk-s3"
|
|
99
|
-
Aws::S3::Client.new(
|
|
124
|
+
Aws::S3::Client.new(**client_options)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Only what is configured is passed, so an unconfigured app builds exactly
|
|
129
|
+
# the client it always did: region alone, the SDK's default credential chain.
|
|
130
|
+
# Keys are passed as a PAIR or not at all; half a pair is a configuration
|
|
131
|
+
# error, and falling back to the default chain would silently write with
|
|
132
|
+
# whatever AWS_* key the dyno happens to hold.
|
|
133
|
+
def client_options
|
|
134
|
+
opts = { region: region }
|
|
135
|
+
opts[:endpoint] = endpoint if endpoint
|
|
136
|
+
id = Studio.s3_access_key_id.to_s
|
|
137
|
+
secret = Studio.s3_secret_access_key.to_s
|
|
138
|
+
if id.empty? != secret.empty?
|
|
139
|
+
raise NotConfigured, "Studio.s3_access_key_id and s3_secret_access_key must be set together"
|
|
140
|
+
end
|
|
141
|
+
unless id.empty?
|
|
142
|
+
opts[:access_key_id] = id
|
|
143
|
+
opts[:secret_access_key] = secret
|
|
100
144
|
end
|
|
145
|
+
opts
|
|
101
146
|
end
|
|
102
147
|
|
|
103
148
|
def reset!
|
data/lib/studio/version.rb
CHANGED
data/lib/studio.rb
CHANGED
|
@@ -616,6 +616,30 @@ module Studio
|
|
|
616
616
|
# A trailing slash is added if you leave it off.
|
|
617
617
|
mattr_accessor :s3_key_prefix, default: nil
|
|
618
618
|
|
|
619
|
+
# S3-COMPATIBLE STORAGE (Cloudflare R2). All four default nil, and nil means
|
|
620
|
+
# AWS exactly as before: the SDK's own endpoint, its default credential chain
|
|
621
|
+
# (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY), and the virtual-hosted
|
|
622
|
+
# amazonaws.com URL. An app moves to R2 by setting them, typically from ENV:
|
|
623
|
+
#
|
|
624
|
+
# config.s3_endpoint = ENV["R2_ENDPOINT"] # https://<account>.r2.cloudflarestorage.com
|
|
625
|
+
# config.s3_region = "auto" # R2's only region
|
|
626
|
+
# config.s3_access_key_id = ENV["R2_ACCESS_KEY_ID"]
|
|
627
|
+
# config.s3_secret_access_key = ENV["R2_SECRET_ACCESS_KEY"]
|
|
628
|
+
# config.s3_public_url = ENV["R2_PUBLIC_URL"] # https://assets.example.com
|
|
629
|
+
#
|
|
630
|
+
# Explicit keys exist so an app can hold its R2 pair BESIDE its AWS pair
|
|
631
|
+
# during a cutover (Active Storage may still mirror to S3) without the two
|
|
632
|
+
# fighting over the same AWS_* variables.
|
|
633
|
+
#
|
|
634
|
+
# s3_public_url is the base public objects are served from — a custom domain
|
|
635
|
+
# or r2.dev URL attached to THIS environment's bucket. R2's S3 endpoint
|
|
636
|
+
# serves nothing anonymously, so with an endpoint and no public base there is
|
|
637
|
+
# no public URL to give, and Studio::S3.url says so rather than inventing one.
|
|
638
|
+
mattr_accessor :s3_endpoint, default: nil
|
|
639
|
+
mattr_accessor :s3_access_key_id, default: nil
|
|
640
|
+
mattr_accessor :s3_secret_access_key, default: nil
|
|
641
|
+
mattr_accessor :s3_public_url, default: nil
|
|
642
|
+
|
|
619
643
|
# Knowledge layer — the S3-backed document store + /admin/knowledge browser
|
|
620
644
|
# (Studio::KnowledgeDoc). Routes are opt-in like every route surface.
|
|
621
645
|
# knowledge_agents is the roster of agent slugs the intake UI offers
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: studio-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.77.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|