bug_bunny 4.7.0 → 4.8.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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/skills/documentation-writer/SKILL.md +45 -0
  3. data/.agents/skills/gem-release/SKILL.md +114 -0
  4. data/.agents/skills/quality-code/SKILL.md +51 -0
  5. data/.agents/skills/rabbitmq-expert/SKILL.md +1555 -0
  6. data/.agents/skills/sentry/SKILL.md +135 -0
  7. data/.agents/skills/sentry/references/api-endpoints.md +147 -0
  8. data/.agents/skills/sentry/scripts/sentry.rb +194 -0
  9. data/.agents/skills/skill-builder/SKILL.md +232 -0
  10. data/.agents/skills/skill-manager/SKILL.md +172 -0
  11. data/.agents/skills/skill-manager/scripts/sync.rb +310 -0
  12. data/.agents/skills/yard/SKILL.md +311 -0
  13. data/.agents/skills/yard/references/tipos.md +144 -0
  14. data/CHANGELOG.md +21 -0
  15. data/CLAUDE.md +29 -220
  16. data/lib/bug_bunny/client.rb +4 -3
  17. data/lib/bug_bunny/controller.rb +10 -14
  18. data/lib/bug_bunny/exception.rb +1 -1
  19. data/lib/bug_bunny/middleware/raise_error.rb +3 -3
  20. data/lib/bug_bunny/observability.rb +5 -5
  21. data/lib/bug_bunny/producer.rb +11 -13
  22. data/lib/bug_bunny/railtie.rb +8 -7
  23. data/lib/bug_bunny/request.rb +3 -11
  24. data/lib/bug_bunny/resource.rb +51 -21
  25. data/lib/bug_bunny/routing/route_set.rb +32 -21
  26. data/lib/bug_bunny/version.rb +1 -1
  27. data/lib/bug_bunny.rb +3 -2
  28. data/lib/generators/bug_bunny/install/install_generator.rb +45 -5
  29. data/lib/tasks/bug_bunny.rake +50 -0
  30. data/skill/SKILL.md +230 -0
  31. data/skill/references/client-middleware.md +144 -0
  32. data/skill/references/consumer.md +104 -0
  33. data/skill/references/controller.md +105 -0
  34. data/skill/references/errores.md +97 -0
  35. data/skill/references/resource.md +116 -0
  36. data/skill/references/routing.md +82 -0
  37. data/skill/references/testing.md +138 -0
  38. data/skills-lock.json +10 -0
  39. data/skills.lock +24 -0
  40. data/skills.yml +19 -0
  41. metadata +27 -17
  42. data/.claude/commands/pr.md +0 -42
  43. data/.claude/commands/release.md +0 -41
  44. data/.claude/commands/rubocop.md +0 -22
  45. data/.claude/commands/test.md +0 -28
  46. data/.claude/commands/yard.md +0 -46
  47. data/docs/concepts.md +0 -140
  48. data/docs/howto/controller.md +0 -194
  49. data/docs/howto/middleware_client.md +0 -119
  50. data/docs/howto/middleware_consumer.md +0 -127
  51. data/docs/howto/rails.md +0 -214
  52. data/docs/howto/resource.md +0 -200
  53. data/docs/howto/routing.md +0 -133
  54. data/docs/howto/testing.md +0 -259
  55. data/docs/howto/tracing.md +0 -119
  56. data/mejoras.md +0 -33
@@ -14,9 +14,7 @@ module BugBunny
14
14
  # 1. Configuración de Autoload
15
15
  initializer 'bug_bunny.add_autoload_paths' do |app|
16
16
  rabbit_path = File.join(app.root, 'app', 'rabbit')
17
- if Dir.exist?(rabbit_path)
18
- app.config.paths.add 'app/rabbit', eager_load: true
19
- end
17
+ app.config.paths.add 'app/rabbit', eager_load: true if Dir.exist?(rabbit_path)
20
18
  end
21
19
 
22
20
  # 2. Gestión de Forks (Puma / Spring / otros)
@@ -25,9 +23,7 @@ module BugBunny
25
23
  # el hijo empiece a trabajar, para evitar compartir el mismo socket TCP.
26
24
  config.after_initialize do
27
25
  # Estrategia 1: Rails 7.1+ ForkTracker (La forma estándar moderna)
28
- if defined?(ActiveSupport::ForkTracker)
29
- ActiveSupport::ForkTracker.after_fork { BugBunny.disconnect }
30
- end
26
+ ActiveSupport::ForkTracker.after_fork { BugBunny.disconnect } if defined?(ActiveSupport::ForkTracker)
31
27
 
32
28
  # Estrategia 2: Hook específico de Puma (Legacy)
33
29
  # Solo intentamos usarlo si la API 'events' está disponible (Puma < 5).
@@ -38,7 +34,12 @@ module BugBunny
38
34
  end
39
35
  end
40
36
 
41
- # 3. Hook de Spring (Preloader)
37
+ # 3. Rake tasks (bug_bunny:sync)
38
+ rake_tasks do
39
+ load File.expand_path('../tasks/bug_bunny.rake', __dir__)
40
+ end
41
+
42
+ # 4. Hook de Spring (Preloader)
42
43
  if defined?(Spring)
43
44
  Spring.after_fork do
44
45
  BugBunny.disconnect
@@ -23,20 +23,11 @@ module BugBunny
23
23
  # @attr exchange_options [Hash] Opciones específicas para la declaración del Exchange en esta petición.
24
24
  # @attr queue_options [Hash] Opciones específicas para la declaración de la Cola en esta petición.
25
25
  class Request
26
- attr_accessor :body
27
- attr_accessor :headers
28
- attr_accessor :params
29
- attr_accessor :path
30
- attr_accessor :method
31
- attr_accessor :exchange
32
- attr_accessor :exchange_type
33
- attr_accessor :routing_key
34
- attr_accessor :timeout
35
- attr_accessor :delivery_mode
26
+ attr_accessor :body, :headers, :params, :path, :method, :exchange, :exchange_type, :routing_key, :timeout,
27
+ :delivery_mode, :queue_options
36
28
 
37
29
  # Configuración de Infraestructura Específica
38
30
  attr_accessor :exchange_options
39
- attr_accessor :queue_options
40
31
 
41
32
  # Metadatos AMQP Estándar
42
33
  attr_accessor :app_id, :content_type, :content_encoding, :priority,
@@ -61,6 +52,7 @@ module BugBunny
61
52
  @exchange_options = {}
62
53
  @queue_options = {}
63
54
  end
55
+
64
56
  # Combina el path con los params como query string.
65
57
  #
66
58
  # @return [String] El path completo con query string si hay params, o solo el path.
@@ -25,8 +25,7 @@ module BugBunny
25
25
  define_model_callbacks :save, :create, :update, :destroy
26
26
 
27
27
  attr_reader :remote_attributes
28
- attr_accessor :persisted
29
- attr_accessor :routing_key, :exchange, :exchange_type
28
+ attr_accessor :persisted, :routing_key, :exchange, :exchange_type
30
29
 
31
30
  # @return [Hash] Opciones específicas de instancia para exchange y queue.
32
31
  attr_accessor :exchange_options, :queue_options
@@ -38,7 +37,9 @@ module BugBunny
38
37
  attr_writer :exchange_options, :queue_options
39
38
 
40
39
  # @api private
41
- def thread_config(key); Thread.current["bb_#{object_id}_#{key}"]; end
40
+ def thread_config(key)
41
+ Thread.current["bb_#{object_id}_#{key}"]
42
+ end
42
43
 
43
44
  # Resuelve la configuración buscando en el hilo, luego en la jerarquía de clases.
44
45
  # @param key [Symbol] Clave en el Thread.current.
@@ -47,29 +48,41 @@ module BugBunny
47
48
  def resolve_config(key, instance_var)
48
49
  val = thread_config(key)
49
50
  return val if val
51
+
50
52
  target = self
51
53
  while target <= BugBunny::Resource
52
54
  value = target.instance_variable_get(instance_var)
53
55
  return value.respond_to?(:call) ? value.call : value unless value.nil?
56
+
54
57
  target = target.superclass
55
58
  end
56
59
  nil
57
60
  end
58
61
 
59
62
  # @return [ConnectionPool, nil]
60
- def connection_pool; resolve_config(:pool, :@connection_pool); end
63
+ def connection_pool
64
+ resolve_config(:pool, :@connection_pool)
65
+ end
61
66
 
62
67
  # @return [String] Nombre del exchange actual.
63
- def current_exchange; resolve_config(:exchange, :@exchange) || raise(ArgumentError, "Exchange not defined for #{name}"); end
68
+ def current_exchange
69
+ resolve_config(:exchange, :@exchange) || raise(ArgumentError, "Exchange not defined for #{name}")
70
+ end
64
71
 
65
72
  # @return [String] Tipo de exchange ('direct', 'topic', 'fanout').
66
- def current_exchange_type; resolve_config(:exchange_type, :@exchange_type) || 'direct'; end
73
+ def current_exchange_type
74
+ resolve_config(:exchange_type, :@exchange_type) || 'direct'
75
+ end
67
76
 
68
77
  # @return [Hash] Opciones de exchange específicas (Nivel 3 de la cascada).
69
- def current_exchange_options; resolve_config(:exchange_options, :@exchange_options) || {}; end
78
+ def current_exchange_options
79
+ resolve_config(:exchange_options, :@exchange_options) || {}
80
+ end
70
81
 
71
82
  # @return [Hash] Opciones de cola específicas.
72
- def current_queue_options; resolve_config(:queue_options, :@queue_options) || {}; end
83
+ def current_queue_options
84
+ resolve_config(:queue_options, :@queue_options) || {}
85
+ end
73
86
 
74
87
  # @return [String] Nombre del recurso para la construcción de rutas.
75
88
  def resource_name
@@ -126,7 +139,8 @@ module BugBunny
126
139
  # @param pool [ConnectionPool] Pool de conexiones.
127
140
  # @param exchange_options [Hash] Opciones de infraestructura.
128
141
  # @param queue_options [Hash] Opciones de cola.
129
- def with(exchange: nil, routing_key: nil, exchange_type: nil, pool: nil, exchange_options: nil, queue_options: nil)
142
+ def with(exchange: nil, routing_key: nil, exchange_type: nil, pool: nil, exchange_options: nil,
143
+ queue_options: nil)
130
144
  keys = {
131
145
  exchange: "bb_#{object_id}_exchange",
132
146
  exchange_type: "bb_#{object_id}_exchange_type",
@@ -163,9 +177,7 @@ module BugBunny
163
177
  end
164
178
 
165
179
  def method_missing(method, *args, &block)
166
- if @used
167
- ::Kernel.raise ::BugBunny::Error, "ScopeProxy is single-use. Call .with again for a new context."
168
- end
180
+ ::Kernel.raise ::BugBunny::Error, 'ScopeProxy is single-use. Call .with again for a new context.' if @used
169
181
  @used = true
170
182
  @target.public_send(method, *args, &block)
171
183
  ensure
@@ -176,11 +188,13 @@ module BugBunny
176
188
  # Calcula la routing key final.
177
189
  # @param id [String, nil] ID del recurso.
178
190
  # @return [String]
179
- def calculate_routing_key(id = nil)
191
+ def calculate_routing_key(_id = nil)
180
192
  manual_rk = thread_config(:routing_key)
181
193
  return manual_rk if manual_rk
194
+
182
195
  static_rk = resolve_config(:routing_key, :@routing_key)
183
196
  return static_rk if static_rk.present?
197
+
184
198
  resource_name
185
199
  end
186
200
 
@@ -206,6 +220,7 @@ module BugBunny
206
220
  )
207
221
 
208
222
  return [] unless response['body'].is_a?(Array)
223
+
209
224
  response['body'].map do |attrs|
210
225
  inst = new(attrs)
211
226
  inst.persisted = true
@@ -218,7 +233,9 @@ module BugBunny
218
233
 
219
234
  # Devuelve todos los registros.
220
235
  # @return [Array<BugBunny::Resource>]
221
- def all; where({}); end
236
+ def all
237
+ where({})
238
+ end
222
239
 
223
240
  # Busca un registro por ID (GET).
224
241
  # Mapea un 404 (NotFound) devolviendo un objeto nulo.
@@ -275,7 +292,7 @@ module BugBunny
275
292
  @exchange_options = self.class.thread_config(:exchange_options) || self.class.current_exchange_options
276
293
  @queue_options = self.class.thread_config(:queue_options) || self.class.current_queue_options
277
294
 
278
- super(attributes)
295
+ super
279
296
  end
280
297
 
281
298
  # Limpia el rastreo de ActiveModel y nuestro rastreo dinámico interno.
@@ -301,24 +318,35 @@ module BugBunny
301
318
  end
302
319
 
303
320
  # @return [String]
304
- def calculate_routing_key(id=nil); @routing_key || self.class.calculate_routing_key(id); end
321
+ def calculate_routing_key(id = nil)
322
+ @routing_key || self.class.calculate_routing_key(id)
323
+ end
305
324
 
306
325
  # @return [String]
307
- def current_exchange; @exchange || self.class.current_exchange; end
326
+ def current_exchange
327
+ @exchange || self.class.current_exchange
328
+ end
308
329
 
309
330
  # @return [String]
310
- def current_exchange_type; @exchange_type || self.class.current_exchange_type; end
331
+ def current_exchange_type
332
+ @exchange_type || self.class.current_exchange_type
333
+ end
311
334
 
312
335
  # @return [BugBunny::Client]
313
- def bug_bunny_client; self.class.bug_bunny_client; end
336
+ def bug_bunny_client
337
+ self.class.bug_bunny_client
338
+ end
314
339
 
315
340
  # @return [Boolean]
316
- def persisted?; !!@persisted; end
341
+ def persisted?
342
+ !!@persisted
343
+ end
317
344
 
318
345
  # Asignación masiva de atributos.
319
346
  # @param new_attributes [Hash]
320
347
  def assign_attributes(new_attributes)
321
348
  return if new_attributes.nil?
349
+
322
350
  new_attributes.each { |k, v| public_send("#{k}=", v) }
323
351
  end
324
352
 
@@ -375,7 +403,7 @@ module BugBunny
375
403
 
376
404
  def id=(value)
377
405
  if self.class.attribute_names.include?('id')
378
- super(value)
406
+ super
379
407
  else
380
408
  @dynamic_changes << 'id' if @extra_attributes['id'] != value
381
409
  @extra_attributes['id'] = value
@@ -433,6 +461,7 @@ module BugBunny
433
461
  # @return [Boolean]
434
462
  def destroy
435
463
  return false unless persisted?
464
+
436
465
  run_callbacks(:destroy) do
437
466
  path = "#{self.class.resource_name}/#{id}"
438
467
  rk = calculate_routing_key(id)
@@ -459,6 +488,7 @@ module BugBunny
459
488
  # Carga errores remotos en el objeto local (utilizado al recibir 422).
460
489
  def load_remote_rabbit_errors(errors_hash)
461
490
  return if errors_hash.nil? || errors_hash.empty?
491
+
462
492
  if errors_hash.is_a?(String)
463
493
  errors.add(:base, errors_hash)
464
494
  else
@@ -57,27 +57,37 @@ module BugBunny
57
57
  # Registra una ruta para el verbo GET.
58
58
  # @param path [String, Symbol] Patrón de la URL.
59
59
  # @param to [String, nil] Destino (controlador#accion). Si es nil, se infiere del scope.
60
- def get(path, to: nil); add_route('GET', path, to: to); end
60
+ def get(path, to: nil)
61
+ add_route('GET', path, to: to)
62
+ end
61
63
 
62
64
  # Registra una ruta para el verbo POST.
63
65
  # @param path [String, Symbol] Patrón de la URL.
64
66
  # @param to [String, nil] Destino (controlador#accion). Si es nil, se infiere del scope.
65
- def post(path, to: nil); add_route('POST', path, to: to); end
67
+ def post(path, to: nil)
68
+ add_route('POST', path, to: to)
69
+ end
66
70
 
67
71
  # Registra una ruta para el verbo PUT.
68
72
  # @param path [String, Symbol] Patrón de la URL.
69
73
  # @param to [String, nil] Destino (controlador#accion). Si es nil, se infiere del scope.
70
- def put(path, to: nil); add_route('PUT', path, to: to); end
74
+ def put(path, to: nil)
75
+ add_route('PUT', path, to: to)
76
+ end
71
77
 
72
78
  # Registra una ruta para el verbo PATCH.
73
79
  # @param path [String, Symbol] Patrón de la URL.
74
80
  # @param to [String, nil] Destino (controlador#accion). Si es nil, se infiere del scope.
75
- def patch(path, to: nil); add_route('PATCH', path, to: to); end
81
+ def patch(path, to: nil)
82
+ add_route('PATCH', path, to: to)
83
+ end
76
84
 
77
85
  # Registra una ruta para el verbo DELETE.
78
86
  # @param path [String, Symbol] Patrón de la URL.
79
87
  # @param to [String, nil] Destino (controlador#accion). Si es nil, se infiere del scope.
80
- def delete(path, to: nil); add_route('DELETE', path, to: to); end
88
+ def delete(path, to: nil)
89
+ add_route('DELETE', path, to: to)
90
+ end
81
91
 
82
92
  # @!endgroup
83
93
 
@@ -113,7 +123,7 @@ module BugBunny
113
123
  resource_path = name.to_s
114
124
 
115
125
  # Todas las acciones estándar disponibles
116
- actions = [:index, :show, :create, :update, :destroy]
126
+ actions = %i[index show create update destroy]
117
127
 
118
128
  # Aplicamos los filtros si existen
119
129
  if only
@@ -131,10 +141,10 @@ module BugBunny
131
141
  delete "#{resource_path}/:id", to: "#{resource_path}#destroy" if actions.include?(:destroy)
132
142
 
133
143
  # Si se pasa un bloque, abrimos un Scope de Recurso para rutas anidadas
134
- if block_given?
135
- with_scope({ type: :resource, name: resource_path }) do
136
- instance_eval(&block)
137
- end
144
+ return unless block_given?
145
+
146
+ with_scope({ type: :resource, name: resource_path }) do
147
+ instance_eval(&block)
138
148
  end
139
149
  end
140
150
 
@@ -197,16 +207,16 @@ module BugBunny
197
207
  # @return [Hash, nil] Hash con `:controller`, `:action`, `:params` y `:namespace`, o `nil` si no hay match.
198
208
  def recognize(method, path)
199
209
  @routes.each do |route|
200
- if route.match?(method, path)
201
- extracted_params = route.extract_params(path)
202
-
203
- return {
204
- controller: route.controller,
205
- action: route.action,
206
- params: extracted_params,
207
- namespace: route.namespace
208
- }
209
- end
210
+ next unless route.match?(method, path)
211
+
212
+ extracted_params = route.extract_params(path)
213
+
214
+ return {
215
+ controller: route.controller,
216
+ action: route.action,
217
+ params: extracted_params,
218
+ namespace: route.namespace
219
+ }
210
220
  end
211
221
 
212
222
  # Si llegamos aquí, es un 404 seguro.
@@ -238,7 +248,8 @@ module BugBunny
238
248
  end
239
249
 
240
250
  if final_to.nil?
241
- raise ArgumentError, "Falta el destino 'to:' para la ruta #{method} '#{final_path}'. Usa la sintaxis 'controlador#accion'"
251
+ raise ArgumentError,
252
+ "Falta el destino 'to:' para la ruta #{method} '#{final_path}'. Usa la sintaxis 'controlador#accion'"
242
253
  end
243
254
 
244
255
  @routes << Route.new(method, final_path, to: final_to, namespace: current_namespace)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BugBunny
4
- VERSION = "4.7.0"
4
+ VERSION = '4.8.1'
5
5
  end
data/lib/bug_bunny.rb CHANGED
@@ -25,6 +25,7 @@ require_relative 'bug_bunny/railtie' if defined?(Rails)
25
25
  # Actúa como espacio de nombres y punto de configuración global.
26
26
  module BugBunny
27
27
  extend BugBunny::Observability
28
+
28
29
  private_class_method :safe_log, :exception_metadata, :observability_name
29
30
 
30
31
  class << self
@@ -95,9 +96,9 @@ module BugBunny
95
96
 
96
97
  @global_connection.close if @global_connection.open?
97
98
  @global_connection = nil
98
-
99
+
99
100
  @logger = configuration.logger
100
- safe_log(:info, "bug_bunny.disconnect")
101
+ safe_log(:info, 'bug_bunny.disconnect')
101
102
  end
102
103
 
103
104
  # @api private
@@ -20,7 +20,7 @@ module BugBunny
20
20
  # @api private
21
21
  source_root File.expand_path('templates', __dir__)
22
22
 
23
- desc "Instala la configuración inicial de BugBunny y crea la estructura de directorios."
23
+ desc 'Instala la configuración inicial de BugBunny y crea la estructura de directorios.'
24
24
 
25
25
  # Genera el archivo de configuración inicial.
26
26
  # Copia la plantilla `initializer.rb` a `config/initializers/bug_bunny.rb` en la app destino.
@@ -33,15 +33,55 @@ module BugBunny
33
33
  # Crea la estructura de carpetas necesaria para el patrón MVC de BugBunny.
34
34
  #
35
35
  # Genera:
36
- # * `app/rabbit/controllers/`: Directorio donde vivirán los controladores de consumidores.
36
+ # * `app/bug_bunny/controllers/`: Directorio donde vivirán los controladores de consumidores.
37
37
  # * `.keep`: Archivo marcador para asegurar que Git rastree la carpeta aunque esté vacía.
38
38
  #
39
39
  # @return [void]
40
40
  def create_directories
41
- empty_directory "app/rabbit/controllers"
42
- create_file "app/rabbit/controllers/.keep", ""
41
+ empty_directory 'app/bug_bunny/controllers'
42
+ create_file 'app/bug_bunny/controllers/.keep', ''
43
+ end
44
+
45
+ # Escribe el bloque inicial de BugBunny en CLAUDE.md del proyecto consumidor.
46
+ #
47
+ # Si CLAUDE.md no existe, crea uno mínimo con la sección correspondiente.
48
+ # Si ya existe, agrega la sección `## Gemas internas` con el bloque de bug_bunny.
49
+ # En ambos casos, el rake task `bug_bunny:sync` se encarga de las actualizaciones futuras.
50
+ #
51
+ # @return [void]
52
+ def update_claude_md
53
+ spec = Gem::Specification.find_by_name('bug_bunny')
54
+ version = spec.version.to_s
55
+ docs_path = File.join(spec.gem_dir, 'docs', 'ai')
56
+
57
+ block = <<~BLOCK
58
+ ## Gemas internas
59
+
60
+ ### bug_bunny
61
+ - **Version:** #{version}
62
+ - **Docs:** #{docs_path}
63
+ - **Updated:** #{Time.now.strftime('%Y-%m-%d')}
64
+ BLOCK
65
+
66
+ claude_md = File.join(destination_root, 'CLAUDE.md')
67
+
68
+ if File.exist?(claude_md)
69
+ content = File.read(claude_md)
70
+ if content.include?('### bug_bunny')
71
+ say_status :skip, 'CLAUDE.md already has a bug_bunny block — run `rake bug_bunny:sync` to update'
72
+ elsif content.include?('## Gemas internas')
73
+ inject_into_file 'CLAUDE.md', "\n#{block.lines.drop(2).join}", after: "## Gemas internas\n"
74
+ say_status :update, 'Added bug_bunny block to existing ## Gemas internas section in CLAUDE.md'
75
+ else
76
+ append_to_file 'CLAUDE.md', "\n#{block}"
77
+ say_status :update, 'Added ## Gemas internas section to CLAUDE.md'
78
+ end
79
+ else
80
+ create_file 'CLAUDE.md', "# #{Rails.application.class.module_parent_name}\n\n#{block}"
81
+ say_status :create, 'CLAUDE.md created with bug_bunny block'
82
+ end
43
83
 
44
- puts "🐰 BugBunny structure created successfully!"
84
+ say ' Add `bundle exec rake bug_bunny:sync` to bin/setup to keep this block up to date.'
45
85
  end
46
86
  end
47
87
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ namespace :bug_bunny do
6
+ desc 'Sync BugBunny AI docs reference in CLAUDE.md with the installed version'
7
+ task :sync do
8
+ spec = Gem::Specification.find_by_name('bug_bunny')
9
+ version = spec.version.to_s
10
+ docs_path = File.join(spec.gem_dir, 'docs', 'ai')
11
+ claude_md_path = File.join(Dir.pwd, 'CLAUDE.md')
12
+
13
+ content = if File.exist?(claude_md_path)
14
+ File.read(claude_md_path)
15
+ else
16
+ app_name = File.basename(Dir.pwd).split(/[-_]/).map(&:capitalize).join
17
+ puts 'bug_bunny:sync — CLAUDE.md not found, creating it.'
18
+ "# #{app_name}\n"
19
+ end
20
+
21
+ # Idempotent: same version already present, nothing to do
22
+ if content.include?('### bug_bunny') && content.include?("**Version:** #{version}")
23
+ puts "bug_bunny:sync — already at #{version}, nothing to do."
24
+ next
25
+ end
26
+
27
+ block = <<~BLOCK
28
+ ### bug_bunny
29
+ - **Version:** #{version}
30
+ - **Docs:** #{docs_path}
31
+ - **Updated:** #{Date.today}
32
+ BLOCK
33
+
34
+ # Replace existing block if present
35
+ if content.match?(/^### bug_bunny\n/)
36
+ updated = content.gsub(/^### bug_bunny\n(?:- \*\*.*\n)*/, block)
37
+ File.write(claude_md_path, updated)
38
+ puts "bug_bunny:sync — updated to #{version} in CLAUDE.md"
39
+ elsif content.include?('## Gemas internas')
40
+ # Append under existing section
41
+ updated = content.sub(/^## Gemas internas\n/, "## Gemas internas\n\n#{block}")
42
+ File.write(claude_md_path, updated)
43
+ puts "bug_bunny:sync — added #{version} under '## Gemas internas' in CLAUDE.md"
44
+ else
45
+ # Create section at end of file
46
+ File.write(claude_md_path, content.rstrip + "\n\n## Gemas internas\n\n#{block}")
47
+ puts "bug_bunny:sync — added '## Gemas internas' section with #{version} to CLAUDE.md"
48
+ end
49
+ end
50
+ end