codeball 0.2.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/.rubocop.yml +342 -0
- data/.ruby-version +1 -0
- data/LICENSE.txt +21 -0
- data/README.md +25 -0
- data/Rakefile +23 -0
- data/data/wont_pack.md +177 -0
- data/exe/codeball +5 -0
- data/issues.rec +61 -0
- data/lib/codeball/ball.rb +54 -0
- data/lib/codeball/body.rb +9 -0
- data/lib/codeball/border.rb +47 -0
- data/lib/codeball/cli.rb +24 -0
- data/lib/codeball/commands/diff.rb +51 -0
- data/lib/codeball/commands/list.rb +49 -0
- data/lib/codeball/commands/pack.rb +54 -0
- data/lib/codeball/commands/unpack.rb +123 -0
- data/lib/codeball/cursor.rb +115 -0
- data/lib/codeball/destination.rb +76 -0
- data/lib/codeball/entry.rb +107 -0
- data/lib/codeball/error.rb +4 -0
- data/lib/codeball/extraction_result.rb +16 -0
- data/lib/codeball/extraction_summary.rb +17 -0
- data/lib/codeball/footer.rb +9 -0
- data/lib/codeball/header.rb +9 -0
- data/lib/codeball/malformed_ball_error.rb +3 -0
- data/lib/codeball/stream.rb +60 -0
- data/lib/codeball/version.rb +3 -0
- data/lib/codeball.rb +23 -0
- data/lib/command_kit/combined_io.rb +28 -0
- data/lib/command_kit/printing.rb +42 -0
- data/scripts/codeball_xtract +42 -0
- metadata +102 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a794dad4c470aeca5b9eb3d843f08e31931b2a805b7effa130944cb0c1757d4c
|
|
4
|
+
data.tar.gz: 8c35f43034a1c961982b9404a744525ab41cb98b8d3b883800aae575a4232fc0
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e0abbfce874bd19a10e2a095ec2cbc8d8e49c8f0c4059b6b4664fb8617ff6943fd64d5793e14ba783c41b1a8c4d9e2bd3736b0a8279653b79c229c324fcb96f3
|
|
7
|
+
data.tar.gz: e48c6b6449a5b7c0f2a5d606baba454dbed962ab0eade8ecd912cf2803064ac3bc6c5d3116e417aa70a5a021bd5ca7a8dbda4ccf6a468f5c297e8ceb3d30eebb
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# ===========================================================================
|
|
2
|
+
# RuboCop Configuration
|
|
3
|
+
#
|
|
4
|
+
# Base: Stock RuboCop defaults
|
|
5
|
+
# AI guardrails: rubocop-claude plugin (all Claude/ cops + stricter metrics)
|
|
6
|
+
# Performance: rubocop-performance (with chain-hostile cops disabled)
|
|
7
|
+
#
|
|
8
|
+
# Philosophy: idiomatic Ruby, pipeline-style chaining, strict for AI agents,
|
|
9
|
+
# readable for humans.
|
|
10
|
+
# ===========================================================================
|
|
11
|
+
|
|
12
|
+
plugins:
|
|
13
|
+
- rubocop-claude
|
|
14
|
+
- rubocop-performance
|
|
15
|
+
- rubocop-minitest
|
|
16
|
+
- rubocop-md
|
|
17
|
+
- rubocop-rake
|
|
18
|
+
|
|
19
|
+
AllCops:
|
|
20
|
+
NewCops: enable
|
|
21
|
+
TargetRubyVersion: 3.4.8
|
|
22
|
+
|
|
23
|
+
# ===========================================================================
|
|
24
|
+
# Overrides from stock — personal style preferences
|
|
25
|
+
# ===========================================================================
|
|
26
|
+
|
|
27
|
+
# Double quotes everywhere. One less decision to make.
|
|
28
|
+
#
|
|
29
|
+
# # bad
|
|
30
|
+
# name = 'Alice'
|
|
31
|
+
#
|
|
32
|
+
# # good
|
|
33
|
+
# name = "Alice"
|
|
34
|
+
# greeting = "Hello, #{name}"
|
|
35
|
+
Style/StringLiterals:
|
|
36
|
+
EnforcedStyle: double_quotes
|
|
37
|
+
|
|
38
|
+
Style/StringLiteralsInInterpolation:
|
|
39
|
+
EnforcedStyle: double_quotes
|
|
40
|
+
|
|
41
|
+
# Frozen string literal is transitional cruft. Ruby 3.4 has chilled strings,
|
|
42
|
+
# full default freeze is coming in a future Ruby.
|
|
43
|
+
# EnforcedStyle: never actively removes existing magic comments via autocorrect.
|
|
44
|
+
#
|
|
45
|
+
# # bad — autocorrect will strip this
|
|
46
|
+
# # frozen_string_literal: true
|
|
47
|
+
#
|
|
48
|
+
# class Foo
|
|
49
|
+
# end
|
|
50
|
+
#
|
|
51
|
+
# # good
|
|
52
|
+
# class Foo
|
|
53
|
+
# end
|
|
54
|
+
Style/FrozenStringLiteralComment:
|
|
55
|
+
EnforcedStyle: never
|
|
56
|
+
|
|
57
|
+
# Freezing constants breaks objects that need post-assignment setup (e.g.
|
|
58
|
+
# Zeitwerk loaders). Not worth the churn.
|
|
59
|
+
Style/MutableConstant:
|
|
60
|
+
Enabled: false
|
|
61
|
+
|
|
62
|
+
# Pipeline style. Chaining multi-line blocks is the whole point.
|
|
63
|
+
#
|
|
64
|
+
# # good — this is how we write Ruby
|
|
65
|
+
# users
|
|
66
|
+
# .select { it.active? }
|
|
67
|
+
# .map(&:name)
|
|
68
|
+
# .sort
|
|
69
|
+
#
|
|
70
|
+
# # bad — stock rubocop wants you to break this into temp variables
|
|
71
|
+
# active = users.select { it.active? }
|
|
72
|
+
# names = active.map(&:name)
|
|
73
|
+
# names.sort
|
|
74
|
+
Style/MultilineBlockChain:
|
|
75
|
+
Enabled: false
|
|
76
|
+
|
|
77
|
+
# Block delimiters are a taste call. Pipeline code uses braces for chaining,
|
|
78
|
+
# do/end for side effects. No cop captures this nuance.
|
|
79
|
+
#
|
|
80
|
+
# # good — braces for functional transforms
|
|
81
|
+
# users.map { it.name.downcase }
|
|
82
|
+
#
|
|
83
|
+
# # good — do/end for side effects
|
|
84
|
+
# users.each do |user|
|
|
85
|
+
# send_notification(user)
|
|
86
|
+
# log_activity(user)
|
|
87
|
+
# end
|
|
88
|
+
Style/BlockDelimiters:
|
|
89
|
+
Enabled: false
|
|
90
|
+
|
|
91
|
+
# Write arrays like arrays.
|
|
92
|
+
#
|
|
93
|
+
# # bad
|
|
94
|
+
# colors = %w[red green blue]
|
|
95
|
+
# statuses = %i[active inactive pending]
|
|
96
|
+
#
|
|
97
|
+
# # good
|
|
98
|
+
# colors = ["red", "green", "blue"]
|
|
99
|
+
# statuses = [:active, :inactive, :pending]
|
|
100
|
+
Style/WordArray:
|
|
101
|
+
Enabled: false
|
|
102
|
+
|
|
103
|
+
Style/SymbolArray:
|
|
104
|
+
Enabled: false
|
|
105
|
+
|
|
106
|
+
# Argument indentation: consistent 2-space indent, not aligned to first arg.
|
|
107
|
+
#
|
|
108
|
+
# # bad — renaming the method cascades whitespace changes
|
|
109
|
+
# some_method(arg1,
|
|
110
|
+
# arg2,
|
|
111
|
+
# arg3)
|
|
112
|
+
#
|
|
113
|
+
# # good
|
|
114
|
+
# some_method(
|
|
115
|
+
# arg1,
|
|
116
|
+
# arg2,
|
|
117
|
+
# arg3
|
|
118
|
+
# )
|
|
119
|
+
Layout/FirstArgumentIndentation:
|
|
120
|
+
EnforcedStyle: consistent
|
|
121
|
+
|
|
122
|
+
# Dot-aligned chaining. Dots form a visual column.
|
|
123
|
+
# rubocop-claude sets indented — we override back to aligned.
|
|
124
|
+
#
|
|
125
|
+
# # bad (indented)
|
|
126
|
+
# users.where(active: true)
|
|
127
|
+
# .order(:name)
|
|
128
|
+
# .limit(10)
|
|
129
|
+
#
|
|
130
|
+
# # good (aligned)
|
|
131
|
+
# users.where(active: true)
|
|
132
|
+
# .order(:name)
|
|
133
|
+
# .limit(10)
|
|
134
|
+
Layout/MultilineMethodCallIndentation:
|
|
135
|
+
EnforcedStyle: aligned
|
|
136
|
+
|
|
137
|
+
# ===========================================================================
|
|
138
|
+
# Overrides from rubocop-claude — loosen where pipeline style conflicts
|
|
139
|
+
# ===========================================================================
|
|
140
|
+
|
|
141
|
+
# rubocop-claude sets MaxSafeNavigationChain: 1. That's too tight for
|
|
142
|
+
# chaining code at boundaries (controllers, API responses).
|
|
143
|
+
#
|
|
144
|
+
# # bad — 3+ deep, hiding a nil propagation problem
|
|
145
|
+
# user&.profile&.settings&.theme
|
|
146
|
+
#
|
|
147
|
+
# # good — two-step is normal for optional associations
|
|
148
|
+
# user&.profile&.avatar_url
|
|
149
|
+
#
|
|
150
|
+
# # good — trust internal code, no &. needed
|
|
151
|
+
# user.profile.settings.theme
|
|
152
|
+
Claude/NoOverlyDefensiveCode:
|
|
153
|
+
MaxSafeNavigationChain: 2
|
|
154
|
+
|
|
155
|
+
Style/SafeNavigation:
|
|
156
|
+
MaxChainLength: 2
|
|
157
|
+
|
|
158
|
+
# Allow `return a, b` for tuple-style returns.
|
|
159
|
+
# rubocop-claude sets this; declared here to survive load-order surprises.
|
|
160
|
+
#
|
|
161
|
+
# # bad (stock rubocop flags this)
|
|
162
|
+
# def swap(a, b)
|
|
163
|
+
# return b, a
|
|
164
|
+
# end
|
|
165
|
+
#
|
|
166
|
+
# # good (we allow it)
|
|
167
|
+
# def swap(a, b)
|
|
168
|
+
# return b, a
|
|
169
|
+
# end
|
|
170
|
+
#
|
|
171
|
+
# # still flagged — redundant single return
|
|
172
|
+
# def name
|
|
173
|
+
# return @name
|
|
174
|
+
# end
|
|
175
|
+
Style/RedundantReturn:
|
|
176
|
+
AllowMultipleReturnValues: true
|
|
177
|
+
|
|
178
|
+
# ===========================================================================
|
|
179
|
+
# Overrides from rubocop-performance — disable chain-hostile cops
|
|
180
|
+
# ===========================================================================
|
|
181
|
+
|
|
182
|
+
# ChainArrayAllocation flags idiomatic pipelines for microsecond gains.
|
|
183
|
+
# If we need real throughput we parallelize, not uglify.
|
|
184
|
+
#
|
|
185
|
+
# # "bad" according to this cop — but we write this deliberately
|
|
186
|
+
# users.select(&:active?).map(&:name).sort
|
|
187
|
+
#
|
|
188
|
+
# # "good" according to this cop — mutating, unreadable, not our style
|
|
189
|
+
# users.select!(&:active?)
|
|
190
|
+
# users.map!(&:name)
|
|
191
|
+
# users.sort!
|
|
192
|
+
Performance/ChainArrayAllocation:
|
|
193
|
+
Enabled: false
|
|
194
|
+
|
|
195
|
+
# MapMethodChain wants to collapse chained maps into one block.
|
|
196
|
+
# That's the opposite of pipeline decomposition.
|
|
197
|
+
#
|
|
198
|
+
# # "bad" according to this cop — but each step is atomic and named
|
|
199
|
+
# users
|
|
200
|
+
# .map(&:name)
|
|
201
|
+
# .map(&:downcase)
|
|
202
|
+
#
|
|
203
|
+
# # "good" according to this cop — combines concerns into one block
|
|
204
|
+
# users.map { it.name.downcase }
|
|
205
|
+
Performance/MapMethodChain:
|
|
206
|
+
Enabled: false
|
|
207
|
+
|
|
208
|
+
# ===========================================================================
|
|
209
|
+
# Additional tightening — not set by stock or rubocop-claude
|
|
210
|
+
# ===========================================================================
|
|
211
|
+
|
|
212
|
+
# Short blocks push toward small chained steps instead of fat lambdas.
|
|
213
|
+
# Stock default is 25. rubocop-claude doesn't touch it. We want 8.
|
|
214
|
+
#
|
|
215
|
+
# # bad — too much in one block, decompose into a pipeline
|
|
216
|
+
# users.map { |user|
|
|
217
|
+
# name = user.full_name
|
|
218
|
+
# parts = name.split(" ")
|
|
219
|
+
# first = parts.first
|
|
220
|
+
# last = parts.last
|
|
221
|
+
# domain = user.email.split("@").last
|
|
222
|
+
# "#{first}.#{last}@#{domain}".downcase
|
|
223
|
+
# }
|
|
224
|
+
#
|
|
225
|
+
# # good — each step is clear
|
|
226
|
+
# users
|
|
227
|
+
# .map(&:full_name)
|
|
228
|
+
# .map { it.split(" ") }
|
|
229
|
+
# .map { "#{it.first}.#{it.last}" }
|
|
230
|
+
# .map(&:downcase)
|
|
231
|
+
Metrics/BlockLength:
|
|
232
|
+
Max: 8
|
|
233
|
+
CountAsOne:
|
|
234
|
+
- array
|
|
235
|
+
- hash
|
|
236
|
+
- heredoc
|
|
237
|
+
- method_call
|
|
238
|
+
AllowedMethods:
|
|
239
|
+
- command
|
|
240
|
+
- describe
|
|
241
|
+
- context
|
|
242
|
+
- shared_examples
|
|
243
|
+
- shared_examples_for
|
|
244
|
+
- shared_context
|
|
245
|
+
|
|
246
|
+
# Anonymous forwarding (*, **, &) breaks TruffleRuby, JRuby, and
|
|
247
|
+
# Ruby < 3.2. Named args are explicit and portable.
|
|
248
|
+
#
|
|
249
|
+
# # bad — anonymous forwarding, not portable
|
|
250
|
+
# def process(*, **, &)
|
|
251
|
+
# other_method(*, **, &)
|
|
252
|
+
# end
|
|
253
|
+
#
|
|
254
|
+
# # good — named, works everywhere, readable
|
|
255
|
+
# def process(*args, **kwargs, &block)
|
|
256
|
+
# other_method(*args, **kwargs, &block)
|
|
257
|
+
# end
|
|
258
|
+
Style/ArgumentsForwarding:
|
|
259
|
+
Enabled: false
|
|
260
|
+
|
|
261
|
+
# Explicit begin/rescue/end is clearer than implicit method-body rescue.
|
|
262
|
+
# The begin block scopes what's being rescued. Without it, the rescue
|
|
263
|
+
# looks like it belongs to the method signature.
|
|
264
|
+
#
|
|
265
|
+
# # bad — what exactly is being rescued here?
|
|
266
|
+
# def process
|
|
267
|
+
# logger.info("starting")
|
|
268
|
+
# result = dangerous_call
|
|
269
|
+
# logger.info("done")
|
|
270
|
+
# rescue NetworkError => e
|
|
271
|
+
# retry_later(e)
|
|
272
|
+
# end
|
|
273
|
+
#
|
|
274
|
+
# # good — begin scopes the danger
|
|
275
|
+
# def process
|
|
276
|
+
# logger.info("starting")
|
|
277
|
+
# begin
|
|
278
|
+
# result = dangerous_call
|
|
279
|
+
# rescue NetworkError => e
|
|
280
|
+
# retry_later(e)
|
|
281
|
+
# end
|
|
282
|
+
# logger.info("done")
|
|
283
|
+
# end
|
|
284
|
+
Style/RedundantBegin:
|
|
285
|
+
Enabled: false
|
|
286
|
+
|
|
287
|
+
# Classes get rdoc. Run `rake rdoc` and keep it honest.
|
|
288
|
+
#
|
|
289
|
+
# # bad
|
|
290
|
+
# class UserService
|
|
291
|
+
# def call(user) = process(user)
|
|
292
|
+
# end
|
|
293
|
+
#
|
|
294
|
+
# # good
|
|
295
|
+
# # Handles user lifecycle operations including activation,
|
|
296
|
+
# # deactivation, and profile updates.
|
|
297
|
+
# class UserService
|
|
298
|
+
# def call(user) = process(user)
|
|
299
|
+
# end
|
|
300
|
+
Style/Documentation:
|
|
301
|
+
Enabled: true
|
|
302
|
+
Exclude:
|
|
303
|
+
- "spec/**/*"
|
|
304
|
+
- "test/**/*"
|
|
305
|
+
|
|
306
|
+
# Trailing commas in multiline literals and arguments.
|
|
307
|
+
# Cleaner diffs — adding an element doesn't touch the previous line.
|
|
308
|
+
# Also saves keystrokes when appending.
|
|
309
|
+
#
|
|
310
|
+
# # bad
|
|
311
|
+
# method_call(
|
|
312
|
+
# arg1,
|
|
313
|
+
# arg2
|
|
314
|
+
# )
|
|
315
|
+
#
|
|
316
|
+
# # good
|
|
317
|
+
# method_call(
|
|
318
|
+
# arg1,
|
|
319
|
+
# arg2,
|
|
320
|
+
# )
|
|
321
|
+
#
|
|
322
|
+
# # bad
|
|
323
|
+
# hash = {
|
|
324
|
+
# name: "Alice",
|
|
325
|
+
# age: 30
|
|
326
|
+
# }
|
|
327
|
+
#
|
|
328
|
+
# # good
|
|
329
|
+
# hash = {
|
|
330
|
+
# name: "Alice",
|
|
331
|
+
# age: 30,
|
|
332
|
+
# }
|
|
333
|
+
Style/TrailingCommaInArrayLiteral:
|
|
334
|
+
EnforcedStyleForMultiline: comma
|
|
335
|
+
|
|
336
|
+
Style/TrailingCommaInHashLiteral:
|
|
337
|
+
EnforcedStyleForMultiline: comma
|
|
338
|
+
|
|
339
|
+
Style/TrailingCommaInArguments:
|
|
340
|
+
EnforcedStyleForMultiline: comma
|
|
341
|
+
|
|
342
|
+
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4.0.1
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David Gillis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Codeball
|
|
2
|
+
|
|
3
|
+
Bidirectional file bundler for clipboard-friendly LLM workflows
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install the gem and add to the application's Gemfile by executing:
|
|
8
|
+
|
|
9
|
+
$ bundle add codeball
|
|
10
|
+
|
|
11
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
12
|
+
|
|
13
|
+
$ gem install codeball
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
TODO: Write usage instructions here.
|
|
18
|
+
|
|
19
|
+
## Development
|
|
20
|
+
|
|
21
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
|
|
22
|
+
|
|
23
|
+
## License
|
|
24
|
+
|
|
25
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require "bundler/gem_tasks"
|
|
2
|
+
require "minitest/test_task"
|
|
3
|
+
require "rspec/core/rake_task"
|
|
4
|
+
require "rubocop/rake_task"
|
|
5
|
+
require "gempilot/version_task"
|
|
6
|
+
|
|
7
|
+
Gempilot::VersionTask.new
|
|
8
|
+
Minitest::TestTask.create
|
|
9
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
10
|
+
RuboCop::RakeTask.new
|
|
11
|
+
|
|
12
|
+
namespace :zeitwerk do
|
|
13
|
+
desc "Verify all files follow Zeitwerk naming conventions"
|
|
14
|
+
task :validate do
|
|
15
|
+
ruby "-e", <<~RUBY
|
|
16
|
+
require 'codeball'
|
|
17
|
+
Codeball::LOADER.eager_load(force: true)
|
|
18
|
+
puts 'Zeitwerk: All files loaded successfully.'
|
|
19
|
+
RUBY
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
task default: [:test, :spec, :rubocop]
|
data/data/wont_pack.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: turbo-for-rails
|
|
3
|
+
description: >
|
|
4
|
+
Use when writing Rails frontend code with Hotwire (Turbo Drive, Turbo Frames, Turbo Streams),
|
|
5
|
+
Stimulus controllers, React components in Rails, ActionCable real-time features, or configuring
|
|
6
|
+
esbuild/TypeScript/CSS bundling for Rails. Also use when writing Cypress tests for Rails apps.
|
|
7
|
+
Trigger when you see turbo_frame_tag, turbo_stream, data-controller, data-action, data-target,
|
|
8
|
+
stimulus-rails, @hotwired/turbo-rails, createRoot with turbo:load, or Rails + React integration.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Turbo for Rails (Hotwire + React)
|
|
12
|
+
|
|
13
|
+
Hotwire (Turbo + Stimulus) handles 80% of interactivity without writing JavaScript. React is for the 20% needing rich client-side state. Both coexist in one Rails app.
|
|
14
|
+
|
|
15
|
+
## Quick Reference
|
|
16
|
+
|
|
17
|
+
| Task | Hotwire approach | React approach |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| Scoped page update | Turbo Frame (`turbo_frame_tag`) | N/A |
|
|
20
|
+
| Multi-region update | Turbo Stream (`.turbo_stream.erb`) | `useState`/`useReducer` + re-render |
|
|
21
|
+
| Toggle/show/hide | Stimulus controller + CSS class | `useState` + conditional render |
|
|
22
|
+
| Real-time push | `turbo_stream_from` + ActionCable | ActionCable subscription + `dispatch` |
|
|
23
|
+
| Form submission | Standard Rails form (Turbo intercepts) | `fetch` with CSRF token |
|
|
24
|
+
| Complex client state | Not ideal -- use React | `useReducer` or Redux Toolkit |
|
|
25
|
+
|
|
26
|
+
## Turbo Essentials
|
|
27
|
+
|
|
28
|
+
**Drive** -- Always on. Replaces `<body>` on navigation. Use `turbo:load` instead of `DOMContentLoaded`. Disable per-element with `data-turbo="false"`.
|
|
29
|
+
|
|
30
|
+
**Frames** -- Scoped navigation. One frame updated per response. IDs must match between display and form partials.
|
|
31
|
+
```erb
|
|
32
|
+
<%= turbo_frame_tag(dom_id(concert)) do %>
|
|
33
|
+
<%= link_to "Edit", edit_concert_path(concert) %>
|
|
34
|
+
<% end %>
|
|
35
|
+
```
|
|
36
|
+
Key options: `src:` (lazy-load), `loading: "lazy"` (defer until visible), `target: "_top"` (break out).
|
|
37
|
+
|
|
38
|
+
**Streams** -- Multi-region updates. Actions: `append`, `prepend`, `replace`, `update`, `remove`, `before`, `after`. Use `target` (single ID) or `targets` (CSS selector).
|
|
39
|
+
```erb
|
|
40
|
+
<%# app/views/favorites/create.turbo_stream.erb %>
|
|
41
|
+
<%= turbo_stream.append("list", @favorite) %>
|
|
42
|
+
<%= turbo_stream.remove(dom_id(@old)) %>
|
|
43
|
+
<%= turbo_stream.update("count", plain: count) %>
|
|
44
|
+
```
|
|
45
|
+
Controller: `format.turbo_stream` renders `<action>.turbo_stream.erb` without layout.
|
|
46
|
+
|
|
47
|
+
**Frames vs Streams**: Frame = one element, must be `<turbo-frame>`. Stream = any number of elements, any DOM ID, richer actions.
|
|
48
|
+
|
|
49
|
+
**Critical gotchas**: Use `requestSubmit()` not `submit()`. Lazy-load response must omit `src` (infinite loop). `button_to` needs `form: {data: {"turbo-frame": "id"}}`. When ActionCable broadcasts AND controller returns stream, use `format.turbo_stream { head(:ok) }` to prevent double updates.
|
|
50
|
+
|
|
51
|
+
See references/turbo.md for inline edit pattern, all ERB helpers, and complete gotcha list.
|
|
52
|
+
|
|
53
|
+
## Stimulus Essentials
|
|
54
|
+
|
|
55
|
+
File: `app/javascript/controllers/<name>_controller.ts`. HTML: `data-controller="<name>"`.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { Controller } from "@hotwired/stimulus"
|
|
59
|
+
export default class MyController extends Controller {
|
|
60
|
+
static targets = ["output"]
|
|
61
|
+
static values = { count: Number }
|
|
62
|
+
outputTarget: HTMLElement // Must declare for TypeScript
|
|
63
|
+
countValue: number // lowercase type, uppercase in static
|
|
64
|
+
|
|
65
|
+
toggle(): void { this.countValue += 1 }
|
|
66
|
+
countValueChanged(): void { this.outputTarget.innerText = `${this.countValue}` }
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Action format**: `data-action="event->controller#method"` (e.g., `click->css#toggle`).
|
|
71
|
+
**Targets**: `data-<controller>-target="name"` -- generates `nameTarget`, `nameTargets`, `hasNameTarget`.
|
|
72
|
+
**Values**: `data-<controller>-<value>-value="x"` -- generates getter/setter + `<value>Changed` callback.
|
|
73
|
+
**Classes**: `data-<controller>-<token>-class="hidden"` -- decouples CSS from JS.
|
|
74
|
+
|
|
75
|
+
**Key rules**: DOM is the state store. `<value>Changed` fires on connect (no separate `connect()` needed). Multiple controllers on one element: `data-controller="css text"`. Chained actions execute in order. After creating `.ts` files, run `bin/rails stimulus:manifest:update`.
|
|
76
|
+
|
|
77
|
+
**Generic reusable controllers** (configure entirely in HTML): CSS toggle, text toggle, CSS flip, sort (MutationObserver). See references/stimulus.md for full implementations.
|
|
78
|
+
|
|
79
|
+
See references/stimulus.md for lifecycle callbacks, params, cross-controller communication, debounce, and all common mistakes.
|
|
80
|
+
|
|
81
|
+
## React in Rails Essentials
|
|
82
|
+
|
|
83
|
+
Components live in `app/javascript/components/`. Mount on `turbo:load`:
|
|
84
|
+
```tsx
|
|
85
|
+
document.addEventListener("turbo:load", () => {
|
|
86
|
+
const el = document.getElementById("react-element")
|
|
87
|
+
if (el) createRoot(el).render(<App {...parseDataAttrs(el.dataset)} />)
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
Import entry point in `app/javascript/application.js`. Pass server data via `data-*` attributes on the mount `<div>`.
|
|
91
|
+
|
|
92
|
+
**State**: `useState` for simple, `useReducer` for complex (discriminated union actions), Redux Toolkit for app-wide. Reducers must be synchronous -- async in `useEffect` or thunks.
|
|
93
|
+
|
|
94
|
+
**CSRF**: All non-GET `fetch` calls need `X-CSRF-Token` from `document.querySelector("[name='csrf-token']")`.
|
|
95
|
+
|
|
96
|
+
**useEffect rules**: Cannot be async (wrap inner fn). `[]` = mount only. Return cleanup fn for intervals/subscriptions. Never omit dependency array if effect updates state.
|
|
97
|
+
|
|
98
|
+
**Immutable updates**: `setSeatStatuses(prev.map(...))` -- never mutate in place.
|
|
99
|
+
|
|
100
|
+
See references/react.md for useReducer/Redux patterns, styled-components, Context API, and complete gotcha list.
|
|
101
|
+
|
|
102
|
+
## ActionCable / Real-Time
|
|
103
|
+
|
|
104
|
+
**Turbo Streams over ActionCable** (zero JS):
|
|
105
|
+
```erb
|
|
106
|
+
<%= turbo_stream_from(current_user, :favorites) %>
|
|
107
|
+
```
|
|
108
|
+
```ruby
|
|
109
|
+
# Model callback (prefer _later_ variants)
|
|
110
|
+
after_create_commit -> { broadcast_append_later_to(user, :favorites, target: "list") }
|
|
111
|
+
after_destroy_commit -> { broadcast_remove_to(user, :favorites) }
|
|
112
|
+
```
|
|
113
|
+
For multi-region broadcasts, use `Turbo::StreamsChannel.broadcast_stream_to` with `content: ApplicationController.render(...)`. Partials must use locals (no `current_user` -- runs outside request cycle).
|
|
114
|
+
|
|
115
|
+
**Custom channels** (Stimulus or React): Guard against double-subscribe (`if (this.subscription) return`). React subscriptions at module level, not inside `useEffect`. Non-serializable objects (subscriptions) stay outside Redux.
|
|
116
|
+
|
|
117
|
+
**Signed streams for React**: Embed `Turbo::StreamsChannel.signed_stream_name(...)` in a data attribute; subscribe with `channel: "Turbo::StreamsChannel"` + `"signed-stream-name"`.
|
|
118
|
+
|
|
119
|
+
See references/real-time.md for bidirectional channels, Redux thunk integration, and broadcast patterns.
|
|
120
|
+
|
|
121
|
+
## Setup Quick Reference
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# New app (book defaults)
|
|
125
|
+
bundle exec rails new . -a propshaft -j esbuild --database postgresql --skip-test --css tailwind
|
|
126
|
+
|
|
127
|
+
# Key packages
|
|
128
|
+
yarn add react react-dom @types/react @types/react-dom
|
|
129
|
+
yarn add @rails/actioncable @types/rails__actioncable
|
|
130
|
+
yarn add --dev typescript tsc-watch cypress
|
|
131
|
+
|
|
132
|
+
# TypeScript dev loop (Procfile.dev)
|
|
133
|
+
js: yarn dev # tsc-watch -> esbuild on success
|
|
134
|
+
css: yarn build:css --watch
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**tsconfig.json**: Set `"jsx": "react"`, `"noEmit": true` (esbuild transpiles), `"allowSyntheticDefaultImports": true` (Redux).
|
|
138
|
+
|
|
139
|
+
**Tailwind content paths** -- must include `.turbo_stream.erb` and `.tsx`:
|
|
140
|
+
```js
|
|
141
|
+
content: ["./app/views/**/*.(html|turbostream).erb", "./app/javascript/**/*.(js|ts|tsx)"]
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**esbuild + Tailwind CSS conflict**: If importing a CSS package (e.g., animate.css), rename Tailwind output to `tailwind.css` and update `stylesheet_link_tag`.
|
|
145
|
+
|
|
146
|
+
See references/setup-and-bundling.md for esbuild flags, import maps migration, TypeScript types, and full package.json.
|
|
147
|
+
|
|
148
|
+
## Testing (Cypress)
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
yarn add --dev cypress eslint-plugin-cypress
|
|
152
|
+
# Gemfile: gem "cypress-rails", gem "dotenv-rails"
|
|
153
|
+
rails cypress:open # interactive rails cypress:run # headless
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
beforeEach(() => {
|
|
158
|
+
cy.request("/cypress_rails_reset_state")
|
|
159
|
+
cy.request("POST", "/test/log_in_user")
|
|
160
|
+
cy.visit("/concerts/last")
|
|
161
|
+
})
|
|
162
|
+
cy.get("[data-cy=submit]").click()
|
|
163
|
+
cy.get("[data-cy=list]").find("article").should("have.lengthOf", 1)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Use `data-cy` attributes for stable selectors. Use test-only controllers for login/setup via `cy.request`. Only one test should walk through the real login form.
|
|
167
|
+
|
|
168
|
+
See references/testing.md for full Cypress command reference, seed patterns, and debugging techniques.
|
|
169
|
+
|
|
170
|
+
## References
|
|
171
|
+
|
|
172
|
+
- `references/turbo.md` -- Turbo Drive/Frames/Streams, ERB helpers, inline edit pattern, all gotchas
|
|
173
|
+
- `references/stimulus.md` -- Controller structure, actions/targets/values/classes, generic controllers, TypeScript
|
|
174
|
+
- `references/react.md` -- Mounting, hooks, Redux Toolkit, styled-components, CSRF, ActionCable integration
|
|
175
|
+
- `references/real-time.md` -- ActionCable setup, Turbo Stream broadcasts, custom channels, signed streams
|
|
176
|
+
- `references/setup-and-bundling.md` -- rails new, esbuild, TypeScript, Tailwind, Propshaft, import maps
|
|
177
|
+
- `references/testing.md` -- Cypress setup, commands, seed data, test controllers, debugging
|
data/exe/codeball
ADDED
data/issues.rec
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
%rec: Issue
|
|
2
|
+
%key: Id
|
|
3
|
+
%typedef: text_t regexp /^.*$/
|
|
4
|
+
%typedef: Status_t enum open in_progress closed
|
|
5
|
+
%type: Id uuid
|
|
6
|
+
%type: Title line
|
|
7
|
+
%type: Description text_t
|
|
8
|
+
%type: Status Status_t
|
|
9
|
+
%type: Updated date
|
|
10
|
+
%auto: Id Updated
|
|
11
|
+
%mandatory: Title Description Status
|
|
12
|
+
|
|
13
|
+
Id: CD8C2A41-A996-4A20-9E80-762201592415
|
|
14
|
+
Updated: Fri, 20 Mar 2026 22:56:44 -0400
|
|
15
|
+
Title: "codeball pack" fails to pack certain files
|
|
16
|
+
Description: When running "codeball pack" on certain files, like data/wont_pack.md, the command:
|
|
17
|
+
+ 1. fails silently with no error
|
|
18
|
+
+ 2. does not log anything to stdout or stderr
|
|
19
|
+
+ 3. exits with error code 0
|
|
20
|
+
+
|
|
21
|
+
+ This is very problematic, as user has no way of knowing why pack failed, or even if it failed. This issue was discovered after realizing the pack was missing a bunch of files, long after the fact. Catastrophic issue, to say the least.
|
|
22
|
+
Status: closed
|
|
23
|
+
|
|
24
|
+
Id: 439180B4-31A8-4090-860A-125CB517C111
|
|
25
|
+
Updated: Fri, 27 Mar 2026 21:34:19 -0400
|
|
26
|
+
Title: Version number should appear on --version
|
|
27
|
+
Description: Version number is not appearing, instead showing:
|
|
28
|
+
+ codeball --version
|
|
29
|
+
+ codeball: version unknown
|
|
30
|
+
+
|
|
31
|
+
+ An idiomatic solution should lean on CommandKit's version number feature
|
|
32
|
+
Status: closed
|
|
33
|
+
|
|
34
|
+
Id: 631BE27A-2A48-11F1-93E9-FE6CB9572C2D
|
|
35
|
+
Updated: Fri, 27 Mar 2026 21:49:41 -0400
|
|
36
|
+
Title: Fix all rubocop issues
|
|
37
|
+
Description: Many violations are present that claude code is responsible for. They need to be addressed, and no rubocop configuration should be edited unless it would be unreasonable to work around the rule
|
|
38
|
+
Status: closed
|
|
39
|
+
|
|
40
|
+
Id: f2ca5c36-31c2-11f1-bf73-fa9e1a133f8e
|
|
41
|
+
Updated: Mon, 06 Apr 2026 14:14:39 +0000
|
|
42
|
+
Title: Test suite writes to /tmp via Dir.mktmpdir
|
|
43
|
+
Description: Dir.mktmpdir uses Dir.tmpdir to resolve the parent directory. Dir.tmpdir checks in order (per /usr/share/ruby/tmpdir.rb lines 130-135): ENV['TMPDIR'], ENV['TMP'], ENV['TEMP'], Etc.systmpdir, /tmp, then current directory. When none of the env vars are set, it falls back to /tmp. This violates the CLAUDE.md hard rule: NEVER write to /tmp. Affected files: spec/spec_helper.rb (CLIHelper#tmp_dir), spec/codeball/destination_spec.rb, and test/entry_test.rb. All calls have after/teardown cleanup so /tmp is not leaked permanently -- the issue is that writes happen to /tmp at all. Fix: set ENV['TMPDIR'] to a project-local directory in spec_helper.rb and test_helper.rb, or pass an explicit second argument to Dir.mktmpdir.
|
|
44
|
+
Status: closed
|
|
45
|
+
|
|
46
|
+
Id: 2F23ED7C-3499-11F1-8D00-FE6CB9572C2F
|
|
47
|
+
Updated: Fri, 10 Apr 2026 00:53:15 -0400
|
|
48
|
+
Title: `codeball pack` should default to packing all files in dir
|
|
49
|
+
Description: Right now codeball pack does not pack anything when called without args. One constantly has to write the following:
|
|
50
|
+
+ ```zsh
|
|
51
|
+
+ $ codeball pack **/*(.DN)
|
|
52
|
+
+ ```
|
|
53
|
+
+
|
|
54
|
+
+ Given that packing all files in repo or workdir is so common, this should be the default
|
|
55
|
+
Status: open
|
|
56
|
+
|
|
57
|
+
Id: A5290DA8-349A-11F1-BE24-FE6CB9572C2F
|
|
58
|
+
Updated: Fri, 10 Apr 2026 01:03:42 -0400
|
|
59
|
+
Title: Add option to ignore warnings
|
|
60
|
+
Description: Add optarg to ignore warnings, and produce exit code 0 if warnings but no errors occur
|
|
61
|
+
Status: open
|