rubyn-code 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +620 -0
  4. data/db/migrations/000_create_schema_migrations.sql +4 -0
  5. data/db/migrations/001_create_sessions.sql +16 -0
  6. data/db/migrations/002_create_messages.sql +16 -0
  7. data/db/migrations/003_create_tasks.sql +17 -0
  8. data/db/migrations/004_create_task_dependencies.sql +8 -0
  9. data/db/migrations/005_create_memories.sql +44 -0
  10. data/db/migrations/006_create_cost_records.sql +16 -0
  11. data/db/migrations/007_create_hooks.sql +12 -0
  12. data/db/migrations/008_create_skills_cache.sql +8 -0
  13. data/db/migrations/009_create_teams.sql +27 -0
  14. data/db/migrations/010_create_instincts.sql +15 -0
  15. data/exe/rubyn-code +6 -0
  16. data/lib/rubyn_code/agent/conversation.rb +193 -0
  17. data/lib/rubyn_code/agent/loop.rb +517 -0
  18. data/lib/rubyn_code/agent/loop_detector.rb +78 -0
  19. data/lib/rubyn_code/auth/oauth.rb +174 -0
  20. data/lib/rubyn_code/auth/server.rb +126 -0
  21. data/lib/rubyn_code/auth/token_store.rb +153 -0
  22. data/lib/rubyn_code/autonomous/daemon.rb +233 -0
  23. data/lib/rubyn_code/autonomous/idle_poller.rb +111 -0
  24. data/lib/rubyn_code/autonomous/task_claimer.rb +100 -0
  25. data/lib/rubyn_code/background/job.rb +19 -0
  26. data/lib/rubyn_code/background/notifier.rb +44 -0
  27. data/lib/rubyn_code/background/worker.rb +146 -0
  28. data/lib/rubyn_code/cli/app.rb +118 -0
  29. data/lib/rubyn_code/cli/input_handler.rb +79 -0
  30. data/lib/rubyn_code/cli/renderer.rb +205 -0
  31. data/lib/rubyn_code/cli/repl.rb +519 -0
  32. data/lib/rubyn_code/cli/spinner.rb +100 -0
  33. data/lib/rubyn_code/cli/stream_formatter.rb +149 -0
  34. data/lib/rubyn_code/config/defaults.rb +43 -0
  35. data/lib/rubyn_code/config/project_config.rb +120 -0
  36. data/lib/rubyn_code/config/settings.rb +127 -0
  37. data/lib/rubyn_code/context/auto_compact.rb +81 -0
  38. data/lib/rubyn_code/context/compactor.rb +89 -0
  39. data/lib/rubyn_code/context/manager.rb +91 -0
  40. data/lib/rubyn_code/context/manual_compact.rb +87 -0
  41. data/lib/rubyn_code/context/micro_compact.rb +135 -0
  42. data/lib/rubyn_code/db/connection.rb +176 -0
  43. data/lib/rubyn_code/db/migrator.rb +146 -0
  44. data/lib/rubyn_code/db/schema.rb +106 -0
  45. data/lib/rubyn_code/hooks/built_in.rb +124 -0
  46. data/lib/rubyn_code/hooks/registry.rb +99 -0
  47. data/lib/rubyn_code/hooks/runner.rb +88 -0
  48. data/lib/rubyn_code/hooks/user_hooks.rb +90 -0
  49. data/lib/rubyn_code/learning/extractor.rb +191 -0
  50. data/lib/rubyn_code/learning/injector.rb +138 -0
  51. data/lib/rubyn_code/learning/instinct.rb +172 -0
  52. data/lib/rubyn_code/llm/client.rb +218 -0
  53. data/lib/rubyn_code/llm/message_builder.rb +116 -0
  54. data/lib/rubyn_code/llm/streaming.rb +203 -0
  55. data/lib/rubyn_code/mcp/client.rb +139 -0
  56. data/lib/rubyn_code/mcp/config.rb +83 -0
  57. data/lib/rubyn_code/mcp/sse_transport.rb +225 -0
  58. data/lib/rubyn_code/mcp/stdio_transport.rb +196 -0
  59. data/lib/rubyn_code/mcp/tool_bridge.rb +164 -0
  60. data/lib/rubyn_code/memory/models.rb +62 -0
  61. data/lib/rubyn_code/memory/search.rb +181 -0
  62. data/lib/rubyn_code/memory/session_persistence.rb +194 -0
  63. data/lib/rubyn_code/memory/store.rb +199 -0
  64. data/lib/rubyn_code/observability/budget_enforcer.rb +159 -0
  65. data/lib/rubyn_code/observability/cost_calculator.rb +61 -0
  66. data/lib/rubyn_code/observability/models.rb +29 -0
  67. data/lib/rubyn_code/observability/token_counter.rb +42 -0
  68. data/lib/rubyn_code/observability/usage_reporter.rb +140 -0
  69. data/lib/rubyn_code/output/diff_renderer.rb +212 -0
  70. data/lib/rubyn_code/output/formatter.rb +120 -0
  71. data/lib/rubyn_code/permissions/deny_list.rb +49 -0
  72. data/lib/rubyn_code/permissions/policy.rb +59 -0
  73. data/lib/rubyn_code/permissions/prompter.rb +80 -0
  74. data/lib/rubyn_code/permissions/tier.rb +22 -0
  75. data/lib/rubyn_code/protocols/interrupt_handler.rb +95 -0
  76. data/lib/rubyn_code/protocols/plan_approval.rb +67 -0
  77. data/lib/rubyn_code/protocols/shutdown_handshake.rb +109 -0
  78. data/lib/rubyn_code/skills/catalog.rb +70 -0
  79. data/lib/rubyn_code/skills/document.rb +80 -0
  80. data/lib/rubyn_code/skills/loader.rb +57 -0
  81. data/lib/rubyn_code/sub_agents/runner.rb +168 -0
  82. data/lib/rubyn_code/sub_agents/summarizer.rb +57 -0
  83. data/lib/rubyn_code/tasks/dag.rb +208 -0
  84. data/lib/rubyn_code/tasks/manager.rb +212 -0
  85. data/lib/rubyn_code/tasks/models.rb +31 -0
  86. data/lib/rubyn_code/teams/mailbox.rb +128 -0
  87. data/lib/rubyn_code/teams/manager.rb +175 -0
  88. data/lib/rubyn_code/teams/teammate.rb +38 -0
  89. data/lib/rubyn_code/tools/background_run.rb +41 -0
  90. data/lib/rubyn_code/tools/base.rb +84 -0
  91. data/lib/rubyn_code/tools/bash.rb +81 -0
  92. data/lib/rubyn_code/tools/bundle_add.rb +53 -0
  93. data/lib/rubyn_code/tools/bundle_install.rb +41 -0
  94. data/lib/rubyn_code/tools/compact.rb +57 -0
  95. data/lib/rubyn_code/tools/db_migrate.rb +52 -0
  96. data/lib/rubyn_code/tools/edit_file.rb +49 -0
  97. data/lib/rubyn_code/tools/executor.rb +62 -0
  98. data/lib/rubyn_code/tools/git_commit.rb +97 -0
  99. data/lib/rubyn_code/tools/git_diff.rb +61 -0
  100. data/lib/rubyn_code/tools/git_log.rb +59 -0
  101. data/lib/rubyn_code/tools/git_status.rb +59 -0
  102. data/lib/rubyn_code/tools/glob.rb +44 -0
  103. data/lib/rubyn_code/tools/grep.rb +81 -0
  104. data/lib/rubyn_code/tools/load_skill.rb +41 -0
  105. data/lib/rubyn_code/tools/memory_search.rb +77 -0
  106. data/lib/rubyn_code/tools/memory_write.rb +52 -0
  107. data/lib/rubyn_code/tools/rails_generate.rb +54 -0
  108. data/lib/rubyn_code/tools/read_file.rb +38 -0
  109. data/lib/rubyn_code/tools/read_inbox.rb +64 -0
  110. data/lib/rubyn_code/tools/registry.rb +48 -0
  111. data/lib/rubyn_code/tools/review_pr.rb +145 -0
  112. data/lib/rubyn_code/tools/run_specs.rb +75 -0
  113. data/lib/rubyn_code/tools/schema.rb +59 -0
  114. data/lib/rubyn_code/tools/send_message.rb +53 -0
  115. data/lib/rubyn_code/tools/spawn_agent.rb +154 -0
  116. data/lib/rubyn_code/tools/spawn_teammate.rb +168 -0
  117. data/lib/rubyn_code/tools/task.rb +148 -0
  118. data/lib/rubyn_code/tools/web_fetch.rb +108 -0
  119. data/lib/rubyn_code/tools/web_search.rb +196 -0
  120. data/lib/rubyn_code/tools/write_file.rb +30 -0
  121. data/lib/rubyn_code/version.rb +5 -0
  122. data/lib/rubyn_code.rb +203 -0
  123. data/skills/code_quality/fits_in_your_head.md +189 -0
  124. data/skills/code_quality/naming_conventions.md +213 -0
  125. data/skills/code_quality/null_object.md +205 -0
  126. data/skills/code_quality/technical_debt.md +135 -0
  127. data/skills/code_quality/value_objects.md +216 -0
  128. data/skills/code_quality/yagni.md +176 -0
  129. data/skills/design_patterns/adapter.md +191 -0
  130. data/skills/design_patterns/bridge_memento_visitor.md +254 -0
  131. data/skills/design_patterns/builder.md +158 -0
  132. data/skills/design_patterns/command.md +126 -0
  133. data/skills/design_patterns/composite.md +147 -0
  134. data/skills/design_patterns/decorator.md +204 -0
  135. data/skills/design_patterns/facade.md +133 -0
  136. data/skills/design_patterns/factory_method.md +169 -0
  137. data/skills/design_patterns/iterator.md +116 -0
  138. data/skills/design_patterns/mediator.md +133 -0
  139. data/skills/design_patterns/observer.md +177 -0
  140. data/skills/design_patterns/proxy.md +140 -0
  141. data/skills/design_patterns/singleton.md +124 -0
  142. data/skills/design_patterns/state.md +207 -0
  143. data/skills/design_patterns/strategy.md +127 -0
  144. data/skills/design_patterns/template_method.md +173 -0
  145. data/skills/gems/devise.md +365 -0
  146. data/skills/gems/dry_rb.md +186 -0
  147. data/skills/gems/factory_bot.md +268 -0
  148. data/skills/gems/faraday.md +263 -0
  149. data/skills/gems/graphql_ruby.md +514 -0
  150. data/skills/gems/pundit.md +446 -0
  151. data/skills/gems/redis.md +219 -0
  152. data/skills/gems/rubocop.md +257 -0
  153. data/skills/gems/sidekiq.md +360 -0
  154. data/skills/gems/stripe.md +224 -0
  155. data/skills/minitest/assertions.md +185 -0
  156. data/skills/minitest/fixtures.md +238 -0
  157. data/skills/minitest/integration_tests.md +210 -0
  158. data/skills/minitest/mailers_and_jobs.md +218 -0
  159. data/skills/minitest/mocking_stubbing.md +202 -0
  160. data/skills/minitest/service_tests_and_performance.md +246 -0
  161. data/skills/minitest/structure_and_conventions.md +169 -0
  162. data/skills/minitest/system_tests.md +237 -0
  163. data/skills/rails/action_cable.md +160 -0
  164. data/skills/rails/active_record_basics.md +174 -0
  165. data/skills/rails/active_storage.md +242 -0
  166. data/skills/rails/api_design.md +212 -0
  167. data/skills/rails/associations.md +182 -0
  168. data/skills/rails/background_jobs.md +212 -0
  169. data/skills/rails/caching.md +158 -0
  170. data/skills/rails/callbacks.md +135 -0
  171. data/skills/rails/concerns_controllers.md +218 -0
  172. data/skills/rails/concerns_models.md +280 -0
  173. data/skills/rails/controllers.md +190 -0
  174. data/skills/rails/engines.md +201 -0
  175. data/skills/rails/form_objects.md +168 -0
  176. data/skills/rails/hotwire.md +229 -0
  177. data/skills/rails/internationalization.md +192 -0
  178. data/skills/rails/logging.md +198 -0
  179. data/skills/rails/mailers.md +180 -0
  180. data/skills/rails/migrations.md +200 -0
  181. data/skills/rails/multitenancy.md +207 -0
  182. data/skills/rails/n_plus_one.md +151 -0
  183. data/skills/rails/presenters.md +244 -0
  184. data/skills/rails/query_objects.md +177 -0
  185. data/skills/rails/routing.md +194 -0
  186. data/skills/rails/scopes.md +187 -0
  187. data/skills/rails/security.md +233 -0
  188. data/skills/rails/serializers.md +243 -0
  189. data/skills/rails/service_objects.md +184 -0
  190. data/skills/rails/testing_strategy.md +258 -0
  191. data/skills/rails/validations.md +206 -0
  192. data/skills/refactoring/code_smells.md +251 -0
  193. data/skills/refactoring/command_query_separation.md +166 -0
  194. data/skills/refactoring/encapsulate_collection.md +125 -0
  195. data/skills/refactoring/extract_class.md +138 -0
  196. data/skills/refactoring/extract_method.md +185 -0
  197. data/skills/refactoring/replace_conditional.md +211 -0
  198. data/skills/refactoring/value_objects.md +246 -0
  199. data/skills/rspec/build_stubbed.md +199 -0
  200. data/skills/rspec/factory_design.md +206 -0
  201. data/skills/rspec/let_vs_let_bang.md +161 -0
  202. data/skills/rspec/mocking_stubbing.md +209 -0
  203. data/skills/rspec/request_specs.md +212 -0
  204. data/skills/rspec/service_specs.md +262 -0
  205. data/skills/rspec/shared_examples.md +244 -0
  206. data/skills/rspec/system_specs.md +286 -0
  207. data/skills/rspec/test_performance.md +215 -0
  208. data/skills/ruby/blocks_procs_lambdas.md +204 -0
  209. data/skills/ruby/classes.md +155 -0
  210. data/skills/ruby/concurrency.md +194 -0
  211. data/skills/ruby/data_struct_openstruct.md +158 -0
  212. data/skills/ruby/debugging_profiling.md +204 -0
  213. data/skills/ruby/enumerable_patterns.md +168 -0
  214. data/skills/ruby/exception_handling.md +199 -0
  215. data/skills/ruby/file_io.md +217 -0
  216. data/skills/ruby/hashes.md +195 -0
  217. data/skills/ruby/metaprogramming.md +170 -0
  218. data/skills/ruby/modules.md +210 -0
  219. data/skills/ruby/pattern_matching.md +177 -0
  220. data/skills/ruby/regular_expressions.md +166 -0
  221. data/skills/ruby/result_objects.md +200 -0
  222. data/skills/ruby/strings.md +177 -0
  223. data/skills/ruby_project/bundler_dependencies.md +181 -0
  224. data/skills/ruby_project/cli_tools.md +224 -0
  225. data/skills/ruby_project/rake_tasks.md +146 -0
  226. data/skills/ruby_project/structure.md +261 -0
  227. data/skills/sinatra/application_structure.md +241 -0
  228. data/skills/sinatra/middleware_and_deployment.md +221 -0
  229. data/skills/sinatra/testing.md +233 -0
  230. data/skills/solid/dependency_inversion.md +195 -0
  231. data/skills/solid/interface_segregation.md +237 -0
  232. data/skills/solid/liskov_substitution.md +263 -0
  233. data/skills/solid/open_closed.md +212 -0
  234. data/skills/solid/single_responsibility.md +183 -0
  235. metadata +397 -0
@@ -0,0 +1,177 @@
1
+ # Ruby: String Handling
2
+
3
+ ## Pattern
4
+
5
+ Strings are everywhere in Ruby. Use the right tool: frozen string literals for performance, heredocs for multi-line, interpolation for building, and `String` methods over regex when possible.
6
+
7
+ ### Frozen String Literals
8
+
9
+ ```ruby
10
+ # frozen_string_literal: true
11
+
12
+ # This magic comment at the top of the file freezes ALL string literals
13
+ # Frozen strings can't be mutated, which prevents bugs and improves performance
14
+ # (Ruby can reuse the same object instead of allocating new ones)
15
+
16
+ name = "Alice"
17
+ name << " Smith" # => FrozenError: can't modify frozen String
18
+
19
+ # When you need a mutable string, use .dup or unary +
20
+ name = +"Alice" # Mutable copy
21
+ name = "Alice".dup # Same thing, more explicit
22
+ name << " Smith" # Works
23
+
24
+ # RECOMMENDATION: Add `# frozen_string_literal: true` to every Ruby file.
25
+ # Rubocop enforces this by default. Rails 8 enables it in new apps.
26
+ ```
27
+
28
+ ### Interpolation vs Concatenation vs Format
29
+
30
+ ```ruby
31
+ user = "Alice"
32
+ count = 3
33
+
34
+ # GOOD: Interpolation — clearest for simple cases
35
+ "Hello, #{user}! You have #{count} orders."
36
+
37
+ # GOOD: Format for precise number formatting
38
+ format("Total: $%.2f", total / 100.0) # => "Total: $19.99"
39
+ format("Order %06d", order_id) # => "Order 000042"
40
+ format("%s has %d orders", user, count) # => "Alice has 3 orders"
41
+
42
+ # BAD: Concatenation — ugly, slow, error-prone with non-strings
43
+ "Hello, " + user + "! You have " + count.to_s + " orders."
44
+
45
+ # BAD: String interpolation for SQL (security vulnerability)
46
+ User.where("name = '#{params[:name]}'") # SQL INJECTION!
47
+ User.where("name = ?", params[:name]) # Safe: parameterized
48
+ ```
49
+
50
+ ### Heredocs
51
+
52
+ ```ruby
53
+ # Plain heredoc — preserves indentation literally
54
+ sql = <<-SQL
55
+ SELECT *
56
+ FROM orders
57
+ WHERE status = 'pending'
58
+ SQL
59
+
60
+ # Squiggly heredoc (Ruby 2.3+) — strips leading whitespace based on the least-indented line
61
+ message = <<~MSG
62
+ Hello #{user.name},
63
+
64
+ Your order #{order.reference} has shipped!
65
+ Expected delivery: #{order.estimated_delivery.strftime("%B %d")}.
66
+
67
+ Thanks,
68
+ The Rubyn Team
69
+ MSG
70
+ # Result has no leading spaces — perfect for emails and templates
71
+
72
+ # Frozen heredoc
73
+ QUERY = <<~SQL.freeze
74
+ SELECT users.email, COUNT(orders.id) as order_count
75
+ FROM users
76
+ LEFT JOIN orders ON orders.user_id = users.id
77
+ GROUP BY users.email
78
+ SQL
79
+ ```
80
+
81
+ ### Common String Operations
82
+
83
+ ```ruby
84
+ # Presence and blank checks (Rails)
85
+ name = " "
86
+ name.blank? # => true (whitespace only)
87
+ name.present? # => false
88
+ name.presence # => nil (returns nil if blank, self if present)
89
+
90
+ # Use .presence for conditional assignment
91
+ display_name = user.nickname.presence || user.email
92
+ # Instead of: user.nickname.blank? ? user.email : user.nickname
93
+
94
+ # Stripping and squishing
95
+ " hello world ".strip # => "hello world" (leading/trailing only)
96
+ " hello world ".squish # => "hello world" (Rails — collapses internal whitespace too)
97
+
98
+ # Case conversion
99
+ "hello_world".camelize # => "HelloWorld" (Rails)
100
+ "HelloWorld".underscore # => "hello_world" (Rails)
101
+ "hello world".titleize # => "Hello World" (Rails)
102
+ "HELLO".downcase # => "hello"
103
+ "hello".upcase # => "HELLO"
104
+
105
+ # Checking content
106
+ "hello world".include?("world") # => true
107
+ "hello world".start_with?("hello") # => true
108
+ "hello world".end_with?("world") # => true
109
+ "hello world".match?(/\d/) # => false (no digits)
110
+
111
+ # Extracting
112
+ "user@example.com".split("@") # => ["user", "example.com"]
113
+ "ORD-001-2026".split("-", 2) # => ["ORD", "001-2026"] (limit splits)
114
+ "hello world"[0..4] # => "hello"
115
+ "order_12345".delete_prefix("order_") # => "12345" (Ruby 2.5+)
116
+ "file.rb".delete_suffix(".rb") # => "file" (Ruby 2.5+)
117
+
118
+ # Replacing
119
+ "hello world".sub("world", "Ruby") # => "hello Ruby" (first occurrence)
120
+ "aabaa".gsub("a", "x") # => "xxbxx" (all occurrences)
121
+ "hello world".tr("aeiou", "*") # => "h*ll* w*rld" (character-level replace)
122
+
123
+ # Truncation (Rails)
124
+ "A very long description that goes on and on".truncate(30)
125
+ # => "A very long description tha..."
126
+ "A very long description".truncate(20, separator: " ")
127
+ # => "A very long..." (breaks at word boundary)
128
+
129
+ # Parameterize for URLs (Rails)
130
+ "Hello World! It's great.".parameterize # => "hello-world-it-s-great"
131
+ ```
132
+
133
+ ### String Building for Performance
134
+
135
+ ```ruby
136
+ # BAD: Repeated concatenation (creates new string objects each time)
137
+ result = ""
138
+ items.each { |item| result += "#{item.name}: #{item.price}\n" }
139
+
140
+ # GOOD: Array join (one allocation at the end)
141
+ result = items.map { |item| "#{item.name}: #{item.price}" }.join("\n")
142
+
143
+ # GOOD: StringIO for large outputs
144
+ require "stringio"
145
+ buffer = StringIO.new
146
+ items.each { |item| buffer.puts "#{item.name}: #{item.price}" }
147
+ result = buffer.string
148
+
149
+ # GOOD: String interpolation with Array for building
150
+ parts = []
151
+ parts << "Status: #{order.status}"
152
+ parts << "Total: #{order.formatted_total}"
153
+ parts << "Items: #{order.line_items.count}" if order.line_items.loaded?
154
+ parts.join(" | ")
155
+ ```
156
+
157
+ ## Why This Is Good
158
+
159
+ - **Frozen string literals prevent mutation bugs and improve performance.** A frozen string used as a hash key or constant is allocated once and reused.
160
+ - **Interpolation is the Ruby way.** `"Hello, #{name}"` is cleaner, safer, and faster than concatenation.
161
+ - **Squiggly heredocs keep code clean.** Multi-line strings stay properly indented in the source without extra whitespace in the output.
162
+ - **`.presence` eliminates conditional checks.** One method replaces a blank-check ternary.
163
+ - **Array + join beats repeated concatenation.** Concatenation creates N intermediate string objects. Join creates one.
164
+
165
+ ## When To Apply
166
+
167
+ - **`# frozen_string_literal: true`** — every Ruby file, always.
168
+ - **Heredocs** — any string longer than 2 lines.
169
+ - **`.presence`** — any `x.blank? ? default : x` pattern.
170
+ - **`.parameterize`** — any user input going into a URL slug.
171
+ - **`format`** — any numeric formatting (currency, percentages, padded numbers).
172
+
173
+ ## When NOT To Apply
174
+
175
+ - **Don't over-optimize string building.** For 5-10 concatenations, readability wins over performance. `join` matters when you have 1,000+ items.
176
+ - **Don't use `.squish` on content that should preserve whitespace** — code blocks, pre-formatted text, etc.
177
+ - **Don't freeze mutable strings.** If you're building a string incrementally, don't freeze the initial value.
@@ -0,0 +1,181 @@
1
+ # Ruby: Bundler and Dependency Management
2
+
3
+ ## Pattern
4
+
5
+ Use Bundler to manage dependencies. Pin versions carefully, group gems by environment, audit for vulnerabilities, and keep the Gemfile organized and commented.
6
+
7
+ ### Gemfile Organization
8
+
9
+ ```ruby
10
+ source "https://rubygems.org"
11
+ ruby "~> 3.3"
12
+
13
+ # ─── Core ───────────────────────────────────────────
14
+ gem "rails", "~> 8.0"
15
+ gem "pg", "~> 1.5"
16
+ gem "redis", "~> 5.0"
17
+ gem "puma", "~> 6.0"
18
+
19
+ # ─── Authentication & Authorization ─────────────────
20
+ gem "devise", "~> 4.9"
21
+ gem "pundit", "~> 2.3"
22
+
23
+ # ─── Background Jobs ────────────────────────────────
24
+ gem "sidekiq", "~> 7.2"
25
+ gem "sidekiq-cron", "~> 1.12"
26
+
27
+ # ─── API ─────────────────────────────────────────────
28
+ gem "grape", "~> 2.0"
29
+ gem "grape-entity", "~> 1.0"
30
+
31
+ # ─── External Services ──────────────────────────────
32
+ gem "faraday", "~> 2.9"
33
+ gem "anthropic", "~> 0.3" # Claude API client
34
+ gem "stripe", "~> 10.0"
35
+
36
+ # ─── Frontend ────────────────────────────────────────
37
+ gem "turbo-rails", "~> 2.0"
38
+ gem "stimulus-rails", "~> 1.3"
39
+ gem "tailwindcss-rails", "~> 2.6"
40
+
41
+ group :development, :test do
42
+ gem "debug" # Built-in Ruby debugger
43
+ gem "dotenv-rails" # Load .env in development
44
+ gem "factory_bot_rails" # Test factories
45
+ gem "faker" # Generate fake data for seeds
46
+ end
47
+
48
+ group :development do
49
+ gem "bullet" # N+1 query detection
50
+ gem "rack-mini-profiler" # Performance profiling
51
+ gem "web-console" # In-browser Rails console
52
+ gem "rubocop-rails-omakase" # Rails-recommended linting
53
+ gem "annotate" # Schema annotations on models
54
+ gem "strong_migrations" # Catch unsafe migrations
55
+ end
56
+
57
+ group :test do
58
+ gem "capybara" # System tests
59
+ gem "selenium-webdriver" # Browser driver
60
+ gem "webmock" # Stub HTTP requests
61
+ gem "simplecov", require: false # Test coverage
62
+ gem "mocha" # Mocking for Minitest
63
+ end
64
+
65
+ group :production do
66
+ # Production-only gems (monitoring, APM)
67
+ end
68
+ ```
69
+
70
+ ### Version Constraints
71
+
72
+ ```ruby
73
+ # Pessimistic operator (~>) — the RIGHT default
74
+ gem "rails", "~> 8.0" # >= 8.0.0, < 9.0 (major updates require manual bump)
75
+ gem "pg", "~> 1.5" # >= 1.5.0, < 2.0
76
+ gem "sidekiq", "~> 7.2" # >= 7.2.0, < 8.0
77
+
78
+ # Exact pin — for gems where ANY update could break you
79
+ gem "anthropic", "0.3.2" # Exactly this version
80
+
81
+ # No constraint — AVOID (accepts any version, including breaking changes)
82
+ gem "faraday" # BAD: could jump from 2.x to 3.x on bundle update
83
+
84
+ # Greater than — rarely needed
85
+ gem "ruby", ">= 3.1" # For gemspecs only, not Gemfiles
86
+ ```
87
+
88
+ ### Security Auditing
89
+
90
+ ```bash
91
+ # Check for known vulnerabilities in your dependencies
92
+ gem install bundler-audit
93
+ bundle audit check --update
94
+
95
+ # Output:
96
+ # Name: actionpack
97
+ # Version: 7.0.4
98
+ # CVE: CVE-2023-22795
99
+ # Criticality: High
100
+ # Solution: upgrade to ~> 7.0.4.1
101
+
102
+ # Automate in CI
103
+ # .github/workflows/security.yml
104
+ # - run: bundle audit check --update
105
+ ```
106
+
107
+ ### Bundle Commands
108
+
109
+ ```bash
110
+ # Install dependencies
111
+ bundle install
112
+
113
+ # Update a specific gem
114
+ bundle update sidekiq
115
+
116
+ # Update all gems within constraints
117
+ bundle update
118
+
119
+ # Show where a gem is installed
120
+ bundle show faraday
121
+
122
+ # Show dependency tree
123
+ bundle list
124
+
125
+ # Open a gem's source code in your editor
126
+ bundle open faraday
127
+
128
+ # Check for outdated gems
129
+ bundle outdated
130
+
131
+ # Verify Gemfile.lock matches Gemfile
132
+ bundle check
133
+ ```
134
+
135
+ ### Gemfile.lock — Always Commit It
136
+
137
+ ```
138
+ # For APPLICATIONS (Rails apps, Sinatra apps):
139
+ # ✅ ALWAYS commit Gemfile.lock
140
+ # Ensures every developer and CI runs the exact same versions
141
+
142
+ # For GEMS (libraries you publish):
143
+ # ❌ Do NOT commit Gemfile.lock
144
+ # Add to .gitignore — consumers use their own lock file
145
+ # The gemspec defines version constraints, not exact versions
146
+ ```
147
+
148
+ ## Why This Is Good
149
+
150
+ - **Pessimistic version constraints prevent breaking changes.** `~> 8.0` allows patch and minor updates but blocks major version jumps that could break your app.
151
+ - **Groups keep production lean.** Development gems (bullet, rubocop) don't install on production servers.
152
+ - **Comments explain WHY a gem exists.** Future developers know what `anthropic` does without looking it up.
153
+ - **`bundle audit` catches CVEs.** Known vulnerabilities in dependencies are reported before they reach production.
154
+ - **Gemfile.lock ensures reproducibility.** Every environment runs the exact same gem versions.
155
+
156
+ ## Anti-Pattern
157
+
158
+ ```ruby
159
+ # BAD: No version constraints
160
+ gem "rails"
161
+ gem "pg"
162
+ gem "sidekiq"
163
+
164
+ # BAD: All gems in one ungrouped list
165
+ gem "rails"
166
+ gem "debug" # Dev only — shouldn't install in production
167
+ gem "pg"
168
+ gem "capybara" # Test only
169
+ gem "sidekiq"
170
+ gem "webmock" # Test only
171
+
172
+ # BAD: Not committing Gemfile.lock
173
+ # .gitignore
174
+ Gemfile.lock # DON'T DO THIS for applications
175
+ ```
176
+
177
+ ## When To Apply
178
+
179
+ - **Every Ruby project.** Bundler is non-negotiable. Use it from the first line of code.
180
+ - **Run `bundle audit` in CI.** Every build should check for vulnerabilities.
181
+ - **Update dependencies monthly.** `bundle outdated` → update one gem at a time → run tests → commit.
@@ -0,0 +1,224 @@
1
+ # Ruby: CLI Tools with Thor
2
+
3
+ ## Pattern
4
+
5
+ Thor is the CLI framework Rails uses internally. It provides command parsing, subcommands, options, and help generation. Use it for any Ruby CLI tool that has more than 2-3 commands.
6
+
7
+ ```ruby
8
+ # lib/rubyn/cli.rb
9
+ require "thor"
10
+
11
+ module Rubyn
12
+ class CLI < Thor
13
+ desc "init", "Initialize Rubyn in the current project"
14
+ option :api_key, type: :string, desc: "API key (or set RUBYN_API_KEY env var)"
15
+ def init
16
+ api_key = options[:api_key] || ENV["RUBYN_API_KEY"]
17
+ api_key ||= ask("Enter your Rubyn API key:")
18
+
19
+ result = Commands::Init.call(api_key: api_key, project_dir: Dir.pwd)
20
+
21
+ if result.success?
22
+ say "✅ Rubyn initialized.", :green
23
+ say " #{result.project_info}", :cyan
24
+ else
25
+ say "❌ #{result.error}", :red
26
+ exit 1
27
+ end
28
+ end
29
+
30
+ desc "refactor FILE", "Refactor a file toward best practices"
31
+ option :model, type: :string, default: "base", enum: %w[light base pro], desc: "AI model tier"
32
+ option :apply, type: :boolean, default: false, desc: "Auto-apply changes without prompting"
33
+ def refactor(file_path)
34
+ ensure_initialized!
35
+ ensure_file_exists!(file_path)
36
+
37
+ say "🔍 Analyzing #{file_path}...", :cyan
38
+ context = Context::FileResolver.resolve(file_path, project_dir: Dir.pwd)
39
+ say " Loading context: #{context.related_files.map { |f| File.basename(f) }.join(', ')}"
40
+
41
+ result = Commands::Refactor.call(
42
+ file_path: file_path,
43
+ model_tier: options[:model],
44
+ context: context
45
+ )
46
+
47
+ display_streaming_response(result)
48
+ display_credits(result)
49
+
50
+ if options[:apply] || yes?("Apply changes? (y/n)")
51
+ apply_changes(result.changes)
52
+ say "✅ Changes applied.", :green
53
+ else
54
+ say "Changes discarded.", :yellow
55
+ end
56
+ end
57
+
58
+ desc "spec FILE", "Generate tests for a file"
59
+ option :model, type: :string, default: "base", enum: %w[light base pro]
60
+ option :framework, type: :string, enum: %w[rspec minitest], desc: "Override detected test framework"
61
+ def spec(file_path)
62
+ ensure_initialized!
63
+ ensure_file_exists!(file_path)
64
+
65
+ say "🧪 Generating tests for #{file_path}...", :cyan
66
+ result = Commands::Spec.call(file_path: file_path, model_tier: options[:model])
67
+
68
+ display_streaming_response(result)
69
+ display_credits(result)
70
+
71
+ if yes?("Write spec file? (y/n)")
72
+ write_file(result.spec_path, result.spec_content)
73
+ say "✅ Written to #{result.spec_path}", :green
74
+ end
75
+ end
76
+
77
+ desc "review FILE_OR_DIR", "Review code for anti-patterns"
78
+ option :model, type: :string, default: "base", enum: %w[light base pro]
79
+ def review(path)
80
+ ensure_initialized!
81
+
82
+ files = File.directory?(path) ? Dir.glob("#{path}/**/*.rb") : [path]
83
+ say "🔍 Reviewing #{files.count} file(s)...", :cyan
84
+
85
+ result = Commands::Review.call(files: files, model_tier: options[:model])
86
+ display_streaming_response(result)
87
+ display_credits(result)
88
+ end
89
+
90
+ desc "usage", "Show credit balance and recent activity"
91
+ def usage
92
+ ensure_initialized!
93
+
94
+ result = Commands::Usage.call
95
+ say result.formatted_output
96
+ end
97
+
98
+ desc "version", "Show Rubyn version"
99
+ def version
100
+ say "Rubyn #{Rubyn::VERSION}"
101
+ end
102
+
103
+ desc "dashboard", "Open the Rubyn web dashboard"
104
+ def dashboard
105
+ if File.exist?(File.join(Dir.pwd, "config", "routes.rb"))
106
+ say "Dashboard available at http://localhost:3000/rubyn", :cyan
107
+ say "(Make sure your Rails server is running)"
108
+ else
109
+ say "Starting standalone dashboard...", :cyan
110
+ Commands::Dashboard.call(project_dir: Dir.pwd)
111
+ end
112
+ end
113
+
114
+ private
115
+
116
+ def ensure_initialized!
117
+ unless Rubyn::Config.initialized?
118
+ say "❌ Rubyn not initialized. Run `rubyn init` first.", :red
119
+ exit 1
120
+ end
121
+ end
122
+
123
+ def ensure_file_exists!(path)
124
+ unless File.exist?(path)
125
+ say "❌ File not found: #{path}", :red
126
+ exit 1
127
+ end
128
+ end
129
+
130
+ def display_streaming_response(result)
131
+ # StreamHandler renders content in real-time via SSE
132
+ result.stream.each do |chunk|
133
+ print chunk.text # Streams to terminal as it arrives
134
+ end
135
+ puts
136
+ end
137
+
138
+ def display_credits(result)
139
+ say "📊 Used #{result.credits_used} credits (#{result.model_tier}) — #{result.balance_remaining} remaining", :cyan
140
+ end
141
+
142
+ def apply_changes(changes)
143
+ changes.each do |change|
144
+ write_file(change.path, change.content)
145
+ end
146
+ end
147
+
148
+ def write_file(path, content)
149
+ FileUtils.mkdir_p(File.dirname(path))
150
+ File.write(path, content)
151
+ end
152
+ end
153
+ end
154
+ ```
155
+
156
+ ### The Executable
157
+
158
+ ```ruby
159
+ #!/usr/bin/env ruby
160
+ # exe/rubyn
161
+
162
+ require "rubyn"
163
+ Rubyn::CLI.start(ARGV)
164
+ ```
165
+
166
+ ### Gemspec Setup
167
+
168
+ ```ruby
169
+ # rubyn.gemspec
170
+ spec.executables = ["rubyn"]
171
+ spec.bindir = "exe"
172
+ ```
173
+
174
+ ### Thor Features Used
175
+
176
+ ```ruby
177
+ # Options
178
+ option :verbose, type: :boolean, default: false, aliases: "-v"
179
+ option :output, type: :string, default: "terminal", enum: %w[terminal json file]
180
+ option :count, type: :numeric, default: 10
181
+
182
+ # Ask for input
183
+ name = ask("What is your name?")
184
+ password = ask("Password:", echo: false) # Hidden input
185
+
186
+ # Yes/No confirmation
187
+ if yes?("Are you sure?")
188
+ # proceed
189
+ end
190
+
191
+ # Colored output
192
+ say "Success!", :green
193
+ say "Warning!", :yellow
194
+ say "Error!", :red
195
+ say "Info", :cyan
196
+
197
+ # Tables
198
+ print_table([
199
+ ["Model", "Credits", "Status"],
200
+ ["Base", "3", "✅"],
201
+ ["Pro", "7", "✅"]
202
+ ])
203
+
204
+ # Shell commands
205
+ run("bundle exec rspec #{spec_path}") # Runs and shows output
206
+ ```
207
+
208
+ ## Why This Is Good
209
+
210
+ - **Auto-generated help.** `rubyn help`, `rubyn help refactor` — Thor generates help text from `desc` and `option` declarations.
211
+ - **Type-checked options.** `type: :boolean`, `type: :numeric`, `enum: %w[light base pro]` — Thor validates before your code runs.
212
+ - **Consistent interface.** Every CLI tool built with Thor follows the same patterns. Users familiar with Rails generators, Bundler, or any Thor-based CLI feel at home.
213
+ - **Subcommands for free.** Each `desc` + method becomes a subcommand. No routing logic, no argument parsing.
214
+ - **Testable.** `Rubyn::CLI.start(["refactor", "app/controllers/orders_controller.rb", "--model", "pro"])` — invoke CLI commands programmatically in tests.
215
+
216
+ ## When To Apply
217
+
218
+ - **Any CLI tool with 3+ commands.** Thor provides structure, help, and option parsing that OptionParser requires you to build manually.
219
+ - **Gem executables.** Thor is the standard for Ruby gem CLIs (Rails, Bundler, Rubocop all use it).
220
+
221
+ ## When NOT To Apply
222
+
223
+ - **One-off scripts.** A single-purpose script doesn't need Thor. Use `OptionParser` or just `ARGV`.
224
+ - **Two commands.** If the CLI is literally `tool do_thing` and `tool --version`, Thor is overkill.