rails-ai-context 4.3.0 → 4.3.2
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 +58 -8
- data/CLAUDE.md +4 -4
- data/CONTRIBUTING.md +1 -1
- data/README.md +7 -7
- data/SECURITY.md +2 -1
- data/docs/GUIDE.md +3 -3
- data/lib/generators/rails_ai_context/install/install_generator.rb +2 -2
- data/lib/rails_ai_context/configuration.rb +4 -2
- data/lib/rails_ai_context/doctor.rb +6 -1
- data/lib/rails_ai_context/fingerprinter.rb +24 -0
- data/lib/rails_ai_context/introspectors/component_introspector.rb +122 -7
- data/lib/rails_ai_context/introspectors/performance_introspector.rb +31 -26
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +183 -6
- data/lib/rails_ai_context/introspectors/view_introspector.rb +2 -2
- data/lib/rails_ai_context/introspectors/view_template_introspector.rb +61 -8
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +13 -22
- data/lib/rails_ai_context/serializers/claude_serializer.rb +15 -3
- data/lib/rails_ai_context/serializers/context_file_serializer.rb +15 -3
- data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +3 -3
- data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +5 -5
- data/lib/rails_ai_context/serializers/design_system_helper.rb +8 -1
- data/lib/rails_ai_context/serializers/opencode_rules_serializer.rb +3 -3
- data/lib/rails_ai_context/serializers/opencode_serializer.rb +0 -1
- data/lib/rails_ai_context/serializers/tool_guide_helper.rb +15 -9
- data/lib/rails_ai_context/server.rb +8 -1
- data/lib/rails_ai_context/tools/analyze_feature.rb +24 -1
- data/lib/rails_ai_context/tools/base_tool.rb +78 -1
- data/lib/rails_ai_context/tools/dependency_graph.rb +4 -1
- data/lib/rails_ai_context/tools/diagnose.rb +135 -8
- data/lib/rails_ai_context/tools/generate_test.rb +87 -7
- data/lib/rails_ai_context/tools/get_callbacks.rb +27 -4
- data/lib/rails_ai_context/tools/get_component_catalog.rb +11 -2
- data/lib/rails_ai_context/tools/get_context.rb +71 -8
- data/lib/rails_ai_context/tools/get_conventions.rb +59 -0
- data/lib/rails_ai_context/tools/get_design_system.rb +45 -7
- data/lib/rails_ai_context/tools/get_edit_context.rb +3 -2
- data/lib/rails_ai_context/tools/get_env.rb +51 -24
- data/lib/rails_ai_context/tools/get_frontend_stack.rb +100 -9
- data/lib/rails_ai_context/tools/get_model_details.rb +20 -0
- data/lib/rails_ai_context/tools/get_partial_interface.rb +12 -5
- data/lib/rails_ai_context/tools/get_schema.rb +1 -0
- data/lib/rails_ai_context/tools/get_stimulus.rb +13 -7
- data/lib/rails_ai_context/tools/get_turbo_map.rb +35 -2
- data/lib/rails_ai_context/tools/get_view.rb +65 -9
- data/lib/rails_ai_context/tools/migration_advisor.rb +10 -3
- data/lib/rails_ai_context/tools/onboard.rb +413 -27
- data/lib/rails_ai_context/tools/performance_check.rb +45 -28
- data/lib/rails_ai_context/tools/query.rb +28 -2
- data/lib/rails_ai_context/tools/read_logs.rb +4 -1
- data/lib/rails_ai_context/tools/review_changes.rb +27 -17
- data/lib/rails_ai_context/tools/runtime_info.rb +289 -0
- data/lib/rails_ai_context/tools/search_code.rb +23 -4
- data/lib/rails_ai_context/tools/security_scan.rb +7 -1
- data/lib/rails_ai_context/tools/session_context.rb +137 -0
- data/lib/rails_ai_context/tools/validate.rb +5 -0
- data/lib/rails_ai_context/version.rb +1 -1
- metadata +6 -4
|
@@ -212,7 +212,7 @@ module RailsAiContext
|
|
|
212
212
|
prefixed_basename = basename.start_with?("_") ? basename : "_#{basename}"
|
|
213
213
|
unprefixed_basename = basename.delete_prefix("_")
|
|
214
214
|
|
|
215
|
-
extensions = %w[.html.erb .erb .html.haml .haml .html.slim .slim]
|
|
215
|
+
extensions = %w[.html.erb .erb .html.haml .haml .html.slim .slim .rb .json.jbuilder .jbuilder .turbo_stream.erb]
|
|
216
216
|
candidates = []
|
|
217
217
|
|
|
218
218
|
# Try prefixed name with various extensions
|
|
@@ -332,18 +332,24 @@ module RailsAiContext
|
|
|
332
332
|
private_class_method def self.find_render_sites(views_dir, partial, root)
|
|
333
333
|
sites = []
|
|
334
334
|
# Build search names: the partial can be referenced multiple ways
|
|
335
|
+
# Normalize: strip underscore prefix from basename and extensions
|
|
335
336
|
parts = partial.split("/")
|
|
336
337
|
basename = parts.last.delete_prefix("_").sub(/\..*\z/, "")
|
|
337
338
|
dir_prefix = parts[0...-1].join("/")
|
|
338
339
|
|
|
340
|
+
# Build the canonical render name (how Rails references partials in render calls)
|
|
341
|
+
# "shared/_status_badge.html.erb" → "shared/status_badge"
|
|
342
|
+
# "_status_badge" → "status_badge"
|
|
343
|
+
canonical = (dir_prefix.empty? ? basename : "#{dir_prefix}/#{basename}")
|
|
344
|
+
|
|
339
345
|
# Possible render references:
|
|
340
346
|
# render "shared/status_badge"
|
|
341
347
|
# render partial: "shared/status_badge"
|
|
342
348
|
# render "status_badge" (from same directory)
|
|
343
349
|
search_patterns = [
|
|
344
|
-
|
|
345
|
-
basename
|
|
346
|
-
]
|
|
350
|
+
canonical, # shared/status_badge
|
|
351
|
+
basename # status_badge
|
|
352
|
+
].uniq
|
|
347
353
|
|
|
348
354
|
view_files = Dir.glob(File.join(views_dir, "**", "*.{erb,haml,slim}")).sort
|
|
349
355
|
|
|
@@ -357,7 +363,8 @@ module RailsAiContext
|
|
|
357
363
|
content.each_line.with_index(1) do |line, line_num|
|
|
358
364
|
search_patterns.each do |search_name|
|
|
359
365
|
# Match render "partial_name" or render partial: "partial_name"
|
|
360
|
-
|
|
366
|
+
# Allow content before search_name (e.g. "shared/status_badge" matches "status_badge")
|
|
367
|
+
next unless line.match?(/render\s.*["'][^"']*#{Regexp.escape(search_name)}["']/)
|
|
361
368
|
|
|
362
369
|
# For short basename matches, verify directory context
|
|
363
370
|
if search_name == basename && dir_prefix.length > 0
|
|
@@ -43,6 +43,7 @@ module RailsAiContext
|
|
|
43
43
|
)
|
|
44
44
|
|
|
45
45
|
def self.call(table: nil, detail: "standard", limit: nil, offset: 0, format: "markdown", server_context: nil)
|
|
46
|
+
set_call_params(table: table, detail: detail)
|
|
46
47
|
schema = cached_context[:schema]
|
|
47
48
|
return text_response("Schema introspection not available. Add :schema to introspectors.") unless schema
|
|
48
49
|
return text_response("Schema introspection not available: #{schema[:error]}") if schema[:error]
|
|
@@ -17,7 +17,7 @@ module RailsAiContext
|
|
|
17
17
|
detail: {
|
|
18
18
|
type: "string",
|
|
19
19
|
enum: %w[summary standard full],
|
|
20
|
-
description: "Detail level. summary: names + counts. standard:
|
|
20
|
+
description: "Detail level. summary: names + counts. standard: targets + values + actions (default). full: everything including outlets, classes, HTML usage."
|
|
21
21
|
},
|
|
22
22
|
limit: {
|
|
23
23
|
type: "integer",
|
|
@@ -73,14 +73,19 @@ module RailsAiContext
|
|
|
73
73
|
|
|
74
74
|
case detail
|
|
75
75
|
when "summary"
|
|
76
|
-
active = controllers.select { |c| (c[:targets] || []).any? || (c[:actions] || []).any? }
|
|
77
|
-
empty = controllers.reject { |c| (c[:targets] || []).any? || (c[:actions] || []).any? }
|
|
76
|
+
active = controllers.select { |c| (c[:targets] || []).any? || (c[:actions] || []).any? || (c[:values].is_a?(Hash) ? c[:values] : {}).any? }
|
|
77
|
+
empty = controllers.reject { |c| (c[:targets] || []).any? || (c[:actions] || []).any? || (c[:values].is_a?(Hash) ? c[:values] : {}).any? }
|
|
78
78
|
|
|
79
79
|
lines = [ "# Stimulus Controllers (#{total})", "" ]
|
|
80
80
|
active.each do |ctrl|
|
|
81
81
|
targets = (ctrl[:targets] || []).size
|
|
82
|
+
values = (ctrl[:values].is_a?(Hash) ? ctrl[:values] : {}).size
|
|
82
83
|
actions = (ctrl[:actions] || []).size
|
|
83
|
-
|
|
84
|
+
parts = []
|
|
85
|
+
parts << "#{targets} targets" if targets > 0
|
|
86
|
+
parts << "#{values} values" if values > 0
|
|
87
|
+
parts << "#{actions} actions" if actions > 0
|
|
88
|
+
lines << "- **#{ctrl[:name]}** — #{parts.join(', ')}"
|
|
84
89
|
end
|
|
85
90
|
if empty.any?
|
|
86
91
|
names = empty.map { |c| c[:name] }.join(", ")
|
|
@@ -90,13 +95,14 @@ module RailsAiContext
|
|
|
90
95
|
text_response(lines.join("\n"))
|
|
91
96
|
|
|
92
97
|
when "standard"
|
|
93
|
-
active = controllers.select { |c| (c[:targets] || []).any? || (c[:actions] || []).any? }
|
|
94
|
-
empty = controllers.reject { |c| (c[:targets] || []).any? || (c[:actions] || []).any? }
|
|
98
|
+
active = controllers.select { |c| (c[:targets] || []).any? || (c[:actions] || []).any? || (c[:values].is_a?(Hash) ? c[:values] : {}).any? }
|
|
99
|
+
empty = controllers.reject { |c| (c[:targets] || []).any? || (c[:actions] || []).any? || (c[:values].is_a?(Hash) ? c[:values] : {}).any? }
|
|
95
100
|
|
|
96
101
|
lines = [ "# Stimulus Controllers (#{total})", "" ]
|
|
97
102
|
active.each do |ctrl|
|
|
98
103
|
lines << "## #{ctrl[:name]}"
|
|
99
104
|
lines << "- Targets: #{(ctrl[:targets] || []).join(', ')}" if ctrl[:targets]&.any?
|
|
105
|
+
lines << "- Values: #{(ctrl[:values].is_a?(Hash) ? ctrl[:values] : {}).map { |k, v| "#{k} (#{v})" }.join(', ')}" if (ctrl[:values].is_a?(Hash) ? ctrl[:values] : {}).any?
|
|
100
106
|
lines << "- Actions: #{(ctrl[:actions] || []).join(', ')}" if ctrl[:actions]&.any?
|
|
101
107
|
if ctrl[:complexity].is_a?(Hash)
|
|
102
108
|
parts = []
|
|
@@ -110,7 +116,7 @@ module RailsAiContext
|
|
|
110
116
|
end
|
|
111
117
|
if empty.any?
|
|
112
118
|
names = empty.map { |c| c[:name] }.join(", ")
|
|
113
|
-
lines << "_Lifecycle only (no targets/actions): #{names}_"
|
|
119
|
+
lines << "_Lifecycle only (no targets/values/actions): #{names}_"
|
|
114
120
|
end
|
|
115
121
|
|
|
116
122
|
# Cross-controller composition
|
|
@@ -105,7 +105,11 @@ module RailsAiContext
|
|
|
105
105
|
|
|
106
106
|
private_class_method def self.format_summary(model_broadcasts, rb_broadcasts, view_subscriptions, view_frames, warnings, filter_label: nil)
|
|
107
107
|
total_broadcasts = model_broadcasts.size + rb_broadcasts.size
|
|
108
|
+
turbo_data = cached_context[:turbo]
|
|
109
|
+
turbo_stream_response_count = turbo_data.is_a?(Hash) && !turbo_data[:error] ? turbo_data[:turbo_stream_responses]&.size.to_i : 0
|
|
110
|
+
|
|
108
111
|
lines = [ "# Turbo Map", "" ]
|
|
112
|
+
lines << "- **Turbo Stream responses:** #{turbo_stream_response_count} (controller `.turbo_stream.erb` templates)" if turbo_stream_response_count > 0
|
|
109
113
|
lines << "- **Model broadcasts:** #{model_broadcasts.size} (via `broadcasts`, `broadcasts_to`, etc.)"
|
|
110
114
|
lines << "- **Explicit broadcasts:** #{rb_broadcasts.size} (via `broadcast_*_to` calls in .rb files)"
|
|
111
115
|
lines << "- **Stream subscriptions:** #{view_subscriptions.size} (`turbo_stream_from` in views)"
|
|
@@ -195,7 +199,9 @@ module RailsAiContext
|
|
|
195
199
|
lines << ""
|
|
196
200
|
end
|
|
197
201
|
|
|
198
|
-
|
|
202
|
+
has_turbo_stream_responses = turbo_data.is_a?(Hash) && turbo_data[:turbo_stream_responses]&.any?
|
|
203
|
+
|
|
204
|
+
if model_broadcasts.empty? && rb_broadcasts.empty? && view_subscriptions.empty? && view_frames.empty? && !has_turbo_stream_responses
|
|
199
205
|
if filter_label
|
|
200
206
|
lines << "_No Turbo usage matching #{filter_label}. Try without filter to see all Turbo Streams and Frames._"
|
|
201
207
|
else
|
|
@@ -211,6 +217,31 @@ module RailsAiContext
|
|
|
211
217
|
private_class_method def self.format_full(model_broadcasts, rb_broadcasts, view_subscriptions, view_frames, warnings, filter_label: nil)
|
|
212
218
|
lines = [ "# Turbo Map (Full Detail)", "" ]
|
|
213
219
|
|
|
220
|
+
# Turbo Drive Configuration & Stream Responses
|
|
221
|
+
turbo_data = cached_context[:turbo]
|
|
222
|
+
if turbo_data.is_a?(Hash) && !turbo_data[:error]
|
|
223
|
+
drive_parts = []
|
|
224
|
+
drive_parts << "morph: #{turbo_data[:morph_meta] ? 'yes' : 'no'}" unless turbo_data[:morph_meta].nil?
|
|
225
|
+
drive_parts << "permanent elements: #{turbo_data[:permanent_elements].size}" if turbo_data[:permanent_elements]&.any?
|
|
226
|
+
if turbo_data[:turbo_drive_settings].is_a?(Hash) && turbo_data[:turbo_drive_settings].any?
|
|
227
|
+
turbo_data[:turbo_drive_settings].each { |k, v| drive_parts << "#{k}: #{v}" }
|
|
228
|
+
end
|
|
229
|
+
if drive_parts.any?
|
|
230
|
+
lines << "## Turbo Drive Configuration"
|
|
231
|
+
drive_parts.each { |p| lines << "- #{p}" }
|
|
232
|
+
lines << ""
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Turbo Stream responses
|
|
236
|
+
if turbo_data[:turbo_stream_responses]&.any?
|
|
237
|
+
lines << "## Turbo Stream Responses (#{turbo_data[:turbo_stream_responses].size})"
|
|
238
|
+
turbo_data[:turbo_stream_responses].each do |resp|
|
|
239
|
+
lines << "- `#{resp}`"
|
|
240
|
+
end
|
|
241
|
+
lines << ""
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
214
245
|
# Model broadcasts with full context
|
|
215
246
|
if model_broadcasts.any?
|
|
216
247
|
lines << "## Model Broadcasts (#{model_broadcasts.size})"
|
|
@@ -289,7 +320,9 @@ module RailsAiContext
|
|
|
289
320
|
lines << ""
|
|
290
321
|
end
|
|
291
322
|
|
|
292
|
-
|
|
323
|
+
has_turbo_stream_responses = turbo_data.is_a?(Hash) && turbo_data[:turbo_stream_responses]&.any?
|
|
324
|
+
|
|
325
|
+
if model_broadcasts.empty? && rb_broadcasts.empty? && view_subscriptions.empty? && view_frames.empty? && !has_turbo_stream_responses
|
|
293
326
|
if filter_label
|
|
294
327
|
lines << "_No Turbo usage matching #{filter_label}. Try without filter to see all Turbo Streams and Frames._"
|
|
295
328
|
else
|
|
@@ -87,7 +87,9 @@ module RailsAiContext
|
|
|
87
87
|
ctrl_templates.sort.each do |name, meta|
|
|
88
88
|
parts = meta[:partials]&.any? ? " renders: #{meta[:partials].join(', ')}" : ""
|
|
89
89
|
stim = meta[:stimulus]&.any? ? " stimulus: #{meta[:stimulus].join(', ')}" : ""
|
|
90
|
-
|
|
90
|
+
comps = meta[:components]&.any? ? " components: #{meta[:components].join(', ')}" : ""
|
|
91
|
+
phlex_tag = meta[:phlex] ? " [phlex]" : ""
|
|
92
|
+
lines << "- #{name} (#{meta[:lines]} lines#{phlex_tag})#{parts}#{comps}#{stim}"
|
|
91
93
|
end
|
|
92
94
|
ctrl_partials.sort.each do |name, meta|
|
|
93
95
|
lines << "- #{name} (#{meta[:lines]} lines)"
|
|
@@ -118,13 +120,29 @@ module RailsAiContext
|
|
|
118
120
|
lines << "## #{ctrl}/" unless controller && all_dirs.size == 1
|
|
119
121
|
ctrl_templates.sort.each do |name, meta|
|
|
120
122
|
detail_parts = []
|
|
121
|
-
detail_parts << "renders: #{meta[:partials].join(', ')}" if meta[:partials]&.any?
|
|
122
|
-
detail_parts << "stimulus: #{meta[:stimulus].join(', ')}" if meta[:stimulus]&.any?
|
|
123
123
|
extra = extract_view_metadata(name)
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
|
|
125
|
+
if meta[:phlex]
|
|
126
|
+
# Phlex views: show components, helpers, stimulus, ivars
|
|
127
|
+
# Prefer introspector-level data, fall back to extract_view_metadata
|
|
128
|
+
components = meta[:components]&.any? ? meta[:components] : extra[:components]
|
|
129
|
+
helpers = meta[:helpers]&.any? ? meta[:helpers] : extra[:helpers]
|
|
130
|
+
detail_parts << "ivars: #{extra[:ivars].join(', ')}" if extra[:ivars]&.any?
|
|
131
|
+
detail_parts << "components: #{components.join(', ')}" if components&.any?
|
|
132
|
+
detail_parts << "helpers: #{helpers.join(', ')}" if helpers&.any?
|
|
133
|
+
detail_parts << "stimulus: #{meta[:stimulus].join(', ')}" if meta[:stimulus]&.any?
|
|
134
|
+
detail_parts << "turbo: #{extra[:turbo].join(', ')}" if extra[:turbo]&.any?
|
|
135
|
+
else
|
|
136
|
+
# ERB/Haml/Slim views: existing behavior
|
|
137
|
+
detail_parts << "renders: #{meta[:partials].join(', ')}" if meta[:partials]&.any?
|
|
138
|
+
detail_parts << "stimulus: #{meta[:stimulus].join(', ')}" if meta[:stimulus]&.any?
|
|
139
|
+
detail_parts << "ivars: #{extra[:ivars].join(', ')}" if extra[:ivars]&.any?
|
|
140
|
+
detail_parts << "turbo: #{extra[:turbo].join(', ')}" if extra[:turbo]&.any?
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
phlex_tag = meta[:phlex] ? " [phlex]" : ""
|
|
126
144
|
details = detail_parts.any? ? " — #{detail_parts.join(' | ')}" : ""
|
|
127
|
-
lines << "- **#{name}** (#{meta[:lines]} lines)#{details}"
|
|
145
|
+
lines << "- **#{name}** (#{meta[:lines]} lines#{phlex_tag})#{details}"
|
|
128
146
|
end
|
|
129
147
|
ctrl_partials.sort.each do |name, meta|
|
|
130
148
|
fields = meta[:fields]&.any? ? " fields: #{meta[:fields].join(', ')}" : ""
|
|
@@ -269,7 +287,7 @@ module RailsAiContext
|
|
|
269
287
|
# Extract instance variables and Turbo wiring from a view template
|
|
270
288
|
private_class_method def self.extract_view_metadata(relative_path)
|
|
271
289
|
content = read_view_content(relative_path)
|
|
272
|
-
return { ivars: [], turbo: [] } if content.nil? || content.include?("(file not found)")
|
|
290
|
+
return { ivars: [], turbo: [], components: [], helpers: [] } if content.nil? || content.include?("(file not found)")
|
|
273
291
|
|
|
274
292
|
# Instance variables used in template
|
|
275
293
|
ivars = content.scan(/@(\w+)/).flatten.uniq.reject { |v| %w[output_buffer virtual_path _request].include?(v) }.sort
|
|
@@ -284,9 +302,47 @@ module RailsAiContext
|
|
|
284
302
|
turbo << "stream:#{val}" unless val.start_with?('"') || val.start_with?("'") || turbo.any? { |t| t.include?(val) }
|
|
285
303
|
end
|
|
286
304
|
|
|
287
|
-
{ ivars: ivars, turbo: turbo.uniq }
|
|
305
|
+
result = { ivars: ivars, turbo: turbo.uniq }
|
|
306
|
+
|
|
307
|
+
# For Phlex views (.rb), extract component renders and helper calls
|
|
308
|
+
if relative_path.end_with?(".rb") && phlex_view_content?(content)
|
|
309
|
+
result[:components] = extract_phlex_components(content)
|
|
310
|
+
result[:helpers] = extract_phlex_helpers(content)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
result
|
|
288
314
|
rescue
|
|
289
|
-
{ ivars: [], turbo: [] }
|
|
315
|
+
{ ivars: [], turbo: [], components: [], helpers: [] }
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Detect if content is a Phlex view class
|
|
319
|
+
private_class_method def self.phlex_view_content?(content)
|
|
320
|
+
content.match?(/class\s+\S+\s*<\s*\S+/) && content.match?(/def\s+view_template\b/)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Extract component render calls from Phlex Ruby DSL
|
|
324
|
+
private_class_method def self.extract_phlex_components(content)
|
|
325
|
+
components = Set.new
|
|
326
|
+
content.scan(/render[\s(]+([A-Z]\w+(?:::\w+)*)\.new/).each do |match|
|
|
327
|
+
components << match[0]
|
|
328
|
+
end
|
|
329
|
+
components.to_a.sort
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Extract helper method calls from Phlex views
|
|
333
|
+
PHLEX_HELPERS = %w[
|
|
334
|
+
link_to image_tag content_for button_to form_with form_for
|
|
335
|
+
content_tag tag number_to_currency number_to_human
|
|
336
|
+
time_ago_in_words distance_of_time_in_words
|
|
337
|
+
truncate pluralize raw sanitize dom_id
|
|
338
|
+
].freeze
|
|
339
|
+
|
|
340
|
+
private_class_method def self.extract_phlex_helpers(content)
|
|
341
|
+
helpers = []
|
|
342
|
+
PHLEX_HELPERS.each do |method|
|
|
343
|
+
helpers << method if content.match?(/\b#{method}\b/)
|
|
344
|
+
end
|
|
345
|
+
helpers
|
|
290
346
|
end
|
|
291
347
|
|
|
292
348
|
# Scan templates that render a partial to extract locals keys
|
|
@@ -108,7 +108,8 @@ module RailsAiContext
|
|
|
108
108
|
private
|
|
109
109
|
|
|
110
110
|
def migration_class_name(action, table, column = nil)
|
|
111
|
-
|
|
111
|
+
preposition = action == "remove" ? "From" : "To"
|
|
112
|
+
parts = [ action.camelize, column&.camelize, preposition, table.camelize ].compact
|
|
112
113
|
parts.join
|
|
113
114
|
end
|
|
114
115
|
|
|
@@ -130,6 +131,8 @@ module RailsAiContext
|
|
|
130
131
|
opts = options ? ", #{options}" : ""
|
|
131
132
|
class_name = migration_class_name("add", table, column)
|
|
132
133
|
|
|
134
|
+
lines << "**Run:** `bin/rails generate migration #{class_name} #{column}:#{type}`"
|
|
135
|
+
lines << ""
|
|
133
136
|
lines << "```ruby"
|
|
134
137
|
lines << "# rails generate migration #{class_name} #{column}:#{type}"
|
|
135
138
|
lines << "class #{class_name} < ActiveRecord::Migration[#{rails_version}]"
|
|
@@ -165,6 +168,8 @@ module RailsAiContext
|
|
|
165
168
|
# Check if column is referenced
|
|
166
169
|
col_type = find_column_type(table, column, schema) || type || "string"
|
|
167
170
|
|
|
171
|
+
lines << "**Run:** `bin/rails generate migration #{class_name} #{column}:#{col_type}`"
|
|
172
|
+
lines << ""
|
|
168
173
|
lines << "**Warning:** `remove_column` is irreversible without specifying the column type."
|
|
169
174
|
lines << ""
|
|
170
175
|
lines << "```ruby"
|
|
@@ -282,6 +287,9 @@ module RailsAiContext
|
|
|
282
287
|
|
|
283
288
|
opts = options ? ", #{options}" : ""
|
|
284
289
|
|
|
290
|
+
# Detect original column type from schema for a reversible down method
|
|
291
|
+
original_type = find_column_type(table, column, cached_context[:schema]) || "string"
|
|
292
|
+
|
|
285
293
|
lines << "**Warning:** Changing column type may cause data loss if types are incompatible."
|
|
286
294
|
lines << ""
|
|
287
295
|
lines << "```ruby"
|
|
@@ -291,8 +299,7 @@ module RailsAiContext
|
|
|
291
299
|
lines << " end"
|
|
292
300
|
lines << ""
|
|
293
301
|
lines << " def down"
|
|
294
|
-
lines << "
|
|
295
|
-
lines << " change_column :#{table}, :#{column}, :original_type"
|
|
302
|
+
lines << " change_column :#{table}, :#{column}, :#{original_type}"
|
|
296
303
|
lines << " end"
|
|
297
304
|
lines << "end"
|
|
298
305
|
lines << "```"
|