rails-mcp-server 1.2.3 → 1.4.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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +168 -166
  3. data/docs/AGENT.md +345 -0
  4. data/exe/rails-mcp-config +1411 -0
  5. data/exe/rails-mcp-server +23 -10
  6. data/exe/rails-mcp-setup-claude +1 -1
  7. data/lib/rails-mcp-server/analyzers/analyze_controller_views.rb +253 -0
  8. data/lib/rails-mcp-server/analyzers/analyze_environment_config.rb +79 -0
  9. data/lib/rails-mcp-server/analyzers/analyze_models.rb +251 -0
  10. data/lib/rails-mcp-server/analyzers/base_analyzer.rb +42 -0
  11. data/lib/rails-mcp-server/analyzers/get_file.rb +40 -0
  12. data/lib/rails-mcp-server/analyzers/get_routes.rb +212 -0
  13. data/lib/rails-mcp-server/analyzers/get_schema.rb +216 -0
  14. data/lib/rails-mcp-server/analyzers/list_files.rb +43 -0
  15. data/lib/rails-mcp-server/analyzers/load_guide.rb +84 -0
  16. data/lib/rails-mcp-server/analyzers/project_info.rb +136 -0
  17. data/lib/rails-mcp-server/tools/base_tool.rb +2 -0
  18. data/lib/rails-mcp-server/tools/execute_ruby.rb +409 -0
  19. data/lib/rails-mcp-server/tools/execute_tool.rb +115 -0
  20. data/lib/rails-mcp-server/tools/search_tools.rb +186 -0
  21. data/lib/rails-mcp-server/tools/switch_project.rb +16 -1
  22. data/lib/rails-mcp-server/version.rb +1 -1
  23. data/lib/rails_mcp_server.rb +19 -53
  24. metadata +65 -18
  25. data/lib/rails-mcp-server/extensions/resource_templating.rb +0 -182
  26. data/lib/rails-mcp-server/extensions/server_templating.rb +0 -333
  27. data/lib/rails-mcp-server/tools/analyze_controller_views.rb +0 -239
  28. data/lib/rails-mcp-server/tools/analyze_environment_config.rb +0 -427
  29. data/lib/rails-mcp-server/tools/analyze_models.rb +0 -116
  30. data/lib/rails-mcp-server/tools/get_file.rb +0 -55
  31. data/lib/rails-mcp-server/tools/get_routes.rb +0 -24
  32. data/lib/rails-mcp-server/tools/get_schema.rb +0 -141
  33. data/lib/rails-mcp-server/tools/list_files.rb +0 -54
  34. data/lib/rails-mcp-server/tools/load_guide.rb +0 -370
  35. data/lib/rails-mcp-server/tools/project_info.rb +0 -86
@@ -1,141 +0,0 @@
1
- module RailsMcpServer
2
- class GetSchema < BaseTool
3
- tool_name "get_schema"
4
-
5
- description "Retrieve database schema information for the Rails application. Without parameters, returns all tables and the complete schema.rb. With a table name, returns detailed column information including data types, constraints, and foreign keys for that specific table."
6
-
7
- arguments do
8
- optional(:table_name).filled(:string).description("Database table name to get detailed schema information for (e.g., 'users', 'products'). Use snake_case, plural form. If omitted, returns complete database schema.")
9
- end
10
-
11
- def call(table_name: nil)
12
- unless current_project
13
- message = "No active project. Please switch to a project first."
14
- log(:warn, message)
15
-
16
- return message
17
- end
18
-
19
- if table_name
20
- log(:info, "Getting schema for table: #{table_name}")
21
-
22
- # Execute the Rails schema command for a specific table
23
- schema_output = RailsMcpServer::RunProcess.execute_rails_command(
24
- active_project_path,
25
- "bin/rails runner \"require 'active_record'; puts ActiveRecord::Base.connection.columns('#{table_name}').map{|c| [c.name, c.type, c.null, c.default].inspect}.join('\\n')\""
26
- )
27
-
28
- if schema_output.strip.empty?
29
- message = "Table '#{table_name}' not found or has no columns."
30
- log(:warn, message)
31
-
32
- return message
33
- end
34
-
35
- # Parse the column information
36
- columns = schema_output.strip.split("\\n").map do |column_info|
37
- eval(column_info) # This is safe because we're generating the string ourselves # rubocop:disable Security/Eval
38
- end
39
-
40
- # Format the output
41
- formatted_columns = columns.map do |name, type, nullable, default|
42
- "#{name} (#{type})#{nullable ? ", nullable" : ""}#{default ? ", default: #{default}" : ""}"
43
- end
44
-
45
- output = <<~SCHEMA
46
- Table: #{table_name}
47
-
48
- Columns:
49
- #{formatted_columns.join("\n")}
50
- SCHEMA
51
-
52
- # Try to get foreign keys
53
- begin
54
- fk_output = RailsMcpServer::RunProcess.execute_rails_command(
55
- active_project_path,
56
- "bin/rails runner \"require 'active_record'; puts ActiveRecord::Base.connection.foreign_keys('#{table_name}').map{|fk| [fk.from_table, fk.to_table, fk.column, fk.primary_key].inspect}.join('\n')\""
57
- )
58
-
59
- unless fk_output.strip.empty?
60
- foreign_keys = fk_output.strip.split("\n").map do |fk_info|
61
- eval(fk_info) # This is safe because we're generating the string ourselves # rubocop:disable Security/Eval
62
- end
63
-
64
- formatted_fks = foreign_keys.map do |from_table, to_table, column, primary_key|
65
- "#{column} -> #{to_table}.#{primary_key}"
66
- end
67
-
68
- output += <<~FK
69
-
70
- Foreign Keys:
71
- #{formatted_fks.join("\n")}
72
- FK
73
- end
74
- rescue => e
75
- log(:warn, "Error fetching foreign keys: #{e.message}")
76
- end
77
-
78
- output
79
- else
80
- log(:info, "Getting full schema")
81
-
82
- # Execute the Rails schema:dump command
83
- # First, check if we need to create the schema file
84
- schema_file = File.join(active_project_path, "db", "schema.rb")
85
- unless File.exist?(schema_file)
86
- log(:info, "Schema file not found, attempting to generate it")
87
- RailsMcpServer::RunProcess.execute_rails_command(active_project_path, "db:schema:dump")
88
- end
89
-
90
- if File.exist?(schema_file)
91
- # Read the schema file
92
- schema_content = File.read(schema_file)
93
-
94
- # Try to get table list
95
- tables_output = RailsMcpServer::RunProcess.execute_rails_command(
96
- active_project_path,
97
- "bin/rails runner \"require 'active_record'; puts ActiveRecord::Base.connection.tables.sort.join('\n')\""
98
- )
99
-
100
- tables = tables_output.strip.split("\n")
101
-
102
- <<~SCHEMA
103
- Database Schema
104
-
105
- Tables:
106
- #{tables.join("\n")}
107
-
108
- Schema Definition:
109
- ```ruby
110
- #{schema_content}
111
- ```
112
- SCHEMA
113
- else
114
- # If we can't get the schema file, try to get the table list
115
- tables_output = RailsMcpServer::RunProcess.execute_rails_command(
116
- active_project_path,
117
- "bin/rails runner \"require 'active_record'; puts ActiveRecord::Base.connection.tables.sort.join('\n')\""
118
- )
119
-
120
- if tables_output.strip.empty?
121
- message = "Could not retrieve schema information. Try running 'rails db:schema:dump' in your project first."
122
- log(:warn, message)
123
-
124
- return message
125
- end
126
-
127
- tables = tables_output.strip.split("\n")
128
-
129
- <<~SCHEMA
130
- Database Schema
131
-
132
- Tables:
133
- #{tables.join("\n")}
134
-
135
- Note: Full schema definition is not available. Run 'rails db:schema:dump' to generate the schema.rb file.
136
- SCHEMA
137
- end
138
- end
139
- end
140
- end
141
- end
@@ -1,54 +0,0 @@
1
- module RailsMcpServer
2
- class ListFiles < BaseTool
3
- tool_name "list_files"
4
-
5
- description "List files in the Rails project matching specific criteria. Use this to explore project directories or locate specific file types. If no parameters are provided, lists files in the project root."
6
-
7
- arguments do
8
- optional(:directory).filled(:string).description("Directory path relative to the project root (e.g., 'app/models', 'config'). Leave empty to list files at the root.")
9
- optional(:pattern).filled(:string).description("File pattern using glob syntax (e.g., '*.rb' for Ruby files, '*.erb' for ERB templates, '*_controller.rb' for controllers)")
10
- end
11
-
12
- def call(directory: "", pattern: "*.rb")
13
- unless current_project
14
- message = "No active project. Please switch to a project first."
15
- log(:warn, message)
16
-
17
- return message
18
- end
19
-
20
- full_path = File.join(active_project_path, directory)
21
- unless File.directory?(full_path)
22
- message = "Directory '#{directory}' not found in the project."
23
- log(:warn, message)
24
-
25
- return message
26
- end
27
-
28
- # Check if this is a git repository
29
- is_git_repo = system("cd #{active_project_path} && git rev-parse --is-inside-work-tree > /dev/null 2>&1")
30
-
31
- if is_git_repo
32
- log(:debug, "Project is a git repository, using git ls-files")
33
-
34
- # Use git ls-files for tracked files
35
- relative_dir = directory.empty? ? "" : "#{directory}/"
36
- git_cmd = "cd #{active_project_path} && git ls-files --cached --others --exclude-standard #{relative_dir}#{pattern}"
37
-
38
- files = `#{git_cmd}`.split("\n").map(&:strip).sort # rubocop:disable Performance/ChainArrayAllocation
39
- else
40
- log(:debug, "Project is not a git repository or git not available, using Dir.glob")
41
-
42
- # Use Dir.glob as fallback
43
- files = Dir.glob(File.join(full_path, pattern))
44
- .map { |f| f.sub("#{active_project_path}/", "") }
45
- .reject { |file| file.start_with?(".git/", ".ruby-lsp/", "node_modules/", "storage/", "public/assets/", "public/packs/", ".bundle/", "vendor/bundle/", "vendor/cache/", "tmp/", "log/") } # rubocop:disable Performance/ChainArrayAllocation
46
- .sort # rubocop:disable Performance/ChainArrayAllocation
47
- end
48
-
49
- log(:debug, "Found #{files.size} files matching pattern (respecting .gitignore and ignoring node_modules)")
50
-
51
- "Files in #{directory.empty? ? "project root" : directory} matching '#{pattern}':\n\n#{files.join("\n")}"
52
- end
53
- end
54
- end
@@ -1,370 +0,0 @@
1
- module RailsMcpServer
2
- class LoadGuide < BaseTool
3
- tool_name "load_guide"
4
-
5
- description "Load documentation guides from Rails, Turbo, Stimulus, Kamal, or Custom. Use this to get guide content for context in conversations."
6
-
7
- arguments do
8
- required(:guides).filled(:string).description("The guides library to search: 'rails', 'turbo', 'stimulus', 'kamal', or 'custom'")
9
- optional(:guide).maybe(:string).description("Specific guide name to load. If not provided, returns available guides list.")
10
- end
11
-
12
- def call(guides:, guide: nil)
13
- # Normalize guides parameter
14
- guides_type = guides.downcase.strip
15
-
16
- # Validate supported guide types
17
- unless %w[rails turbo stimulus kamal custom].include?(guides_type)
18
- message = "Unsupported guide type '#{guides_type}'. Supported types: rails, turbo, stimulus, kamal, custom."
19
- log(:error, message)
20
- return message
21
- end
22
-
23
- if guide.nil? || guide.strip.empty?
24
- log(:debug, "Loading available #{guides_type} guides...")
25
- load_guides_list(guides_type)
26
- else
27
- log(:debug, "Loading specific #{guides_type} guide: #{guide}")
28
- load_specific_guide(guide, guides_type)
29
- end
30
- end
31
-
32
- private
33
-
34
- def load_guides_list(guides_type)
35
- case guides_type
36
- when "rails"
37
- uri = "rails://guides"
38
- read_resource(uri, RailsGuidesResources)
39
- when "stimulus"
40
- uri = "stimulus://guides"
41
- read_resource(uri, StimulusGuidesResources)
42
- when "turbo"
43
- uri = "turbo://guides"
44
- read_resource(uri, TurboGuidesResources)
45
- when "kamal"
46
- uri = "kamal://guides"
47
- read_resource(uri, KamalGuidesResources)
48
- when "custom"
49
- uri = "custom://guides"
50
- read_resource(uri, CustomGuidesResources)
51
- else
52
- "Guide type '#{guides_type}' not supported."
53
- end
54
- end
55
-
56
- def load_specific_guide(guide_name, guides_type)
57
- # First try exact match
58
- exact_match_content = try_exact_match(guide_name, guides_type)
59
- return exact_match_content if exact_match_content && !exact_match_content.include?("Guide not found")
60
-
61
- # If exact match fails, try fuzzy matching
62
- try_fuzzy_matching(guide_name, guides_type)
63
- end
64
-
65
- def try_exact_match(guide_name, guides_type)
66
- case guides_type
67
- when "rails"
68
- uri = "rails://guides/#{guide_name}"
69
- read_resource(uri, RailsGuidesResource, {guide_name: guide_name})
70
- when "stimulus"
71
- uri = "stimulus://guides/#{guide_name}"
72
- read_resource(uri, StimulusGuidesResource, {guide_name: guide_name})
73
- when "turbo"
74
- uri = "turbo://guides/#{guide_name}"
75
- read_resource(uri, TurboGuidesResource, {guide_name: guide_name})
76
- when "kamal"
77
- uri = "kamal://guides/#{guide_name}"
78
- read_resource(uri, KamalGuidesResource, {guide_name: guide_name})
79
- when "custom"
80
- uri = "custom://guides/#{guide_name}"
81
- read_resource(uri, CustomGuidesResource, {guide_name: guide_name})
82
- else
83
- "Guide type '#{guides_type}' not supported."
84
- end
85
- end
86
-
87
- def try_fuzzy_matching(guide_name, guides_type)
88
- # Get all matching guides using the base guide resource directly
89
- matching_guides = find_matching_guides(guide_name, guides_type)
90
-
91
- case matching_guides.size
92
- when 0
93
- format_guide_not_found_message(guide_name, guides_type)
94
- when 1
95
- # Load the single match
96
- match = matching_guides.first
97
- log(:debug, "Found single fuzzy match: #{match}")
98
- try_exact_match(match, guides_type)
99
- when 2..3
100
- # Load multiple matches (up to 3)
101
- log(:debug, "Found #{matching_guides.size} fuzzy matches, loading all")
102
- load_multiple_guides(matching_guides, guides_type, guide_name)
103
- else
104
- # Too many matches, show options
105
- format_multiple_matches_message(guide_name, matching_guides, guides_type)
106
- end
107
- end
108
-
109
- def find_matching_guides(guide_name, guides_type)
110
- # Get the manifest to find matching files
111
- manifest = load_manifest_for_guides_type(guides_type)
112
- return [] unless manifest
113
-
114
- available_guides = manifest["files"].keys.select { |f| f.end_with?(".md") }.map { |f| f.sub(".md", "") } # rubocop:disable Performance/ChainArrayAllocation
115
-
116
- # Generate variations and find matches
117
- variations = generate_guide_name_variations(guide_name, guides_type)
118
- matching_guides = []
119
-
120
- variations.each do |variation|
121
- matches = available_guides.select do |guide|
122
- guide.downcase.include?(variation.downcase) ||
123
- variation.downcase.include?(guide.downcase) ||
124
- guide.gsub(/[_\-\s]/, "").downcase.include?(variation.gsub(/[_\-\s]/, "").downcase)
125
- end
126
- matching_guides.concat(matches)
127
- end
128
-
129
- matching_guides.uniq.sort # rubocop:disable Performance/ChainArrayAllocation
130
- end
131
-
132
- def load_manifest_for_guides_type(guides_type)
133
- config = RailsMcpServer.config
134
- manifest_file = File.join(config.config_dir, "resources", guides_type, "manifest.yaml")
135
-
136
- return nil unless File.exist?(manifest_file)
137
-
138
- YAML.load_file(manifest_file)
139
- rescue => e
140
- log(:error, "Failed to load manifest for #{guides_type}: #{e.message}")
141
- nil
142
- end
143
-
144
- def load_multiple_guides(guide_names, guides_type, original_query)
145
- results = []
146
-
147
- results << "# Multiple Guides Found for '#{original_query}'"
148
- results << ""
149
- results << "Found #{guide_names.size} matching guides. Loading all:\n"
150
-
151
- guide_names.each_with_index do |guide_name, index|
152
- results << "---"
153
- results << ""
154
- results << "## #{index + 1}. #{guide_name}"
155
- results << ""
156
-
157
- content = try_exact_match(guide_name, guides_type)
158
- if content && !content.include?("Guide not found") && !content.include?("Error")
159
- # Remove the header from individual guide content to avoid duplication
160
- clean_content = content.sub(/^#[^\n]*\n/, "").sub(/^\*\*Source:.*?\n---\n/m, "")
161
- results << clean_content.strip
162
- else
163
- results << "*Failed to load this guide*"
164
- end
165
-
166
- results << "" if index < guide_names.size - 1
167
- end
168
-
169
- results.join("\n")
170
- end
171
-
172
- def format_multiple_matches_message(guide_name, matches, guides_type)
173
- message = <<~MSG
174
- # Multiple Guides Found
175
-
176
- Found #{matches.size} guides matching '#{guide_name}' in #{guides_type} guides:
177
-
178
- MSG
179
-
180
- matches.first(10).each_with_index do |match, index|
181
- message += "#{index + 1}. #{match}\n"
182
- end
183
-
184
- if matches.size > 10
185
- message += "... and #{matches.size - 10} more\n"
186
- end
187
-
188
- message += <<~MSG
189
-
190
- ## To load a specific guide, use the exact name:
191
- ```
192
- MSG
193
-
194
- matches.first(3).each do |match|
195
- message += "load_guide guides: \"#{guides_type}\", guide: \"#{match}\"\n"
196
- end
197
-
198
- message += "```\n"
199
- message
200
- end
201
-
202
- def read_resource(uri, resource_class, params = {})
203
- # Check if the resource supports the instance method (from templating extension)
204
- if resource_class.respond_to?(:instance)
205
- instance = resource_class.instance(uri)
206
- return instance.content
207
- end
208
-
209
- # Fallback: manually create instance with proper initialization
210
- create_resource_instance(resource_class, params)
211
- rescue => e
212
- log(:error, "Error reading resource #{uri}: #{e.message}")
213
- format_error_message("Error loading guide: #{e.message}")
214
- end
215
-
216
- def create_resource_instance(resource_class, params)
217
- # Create instance using the proper pattern for FastMcp resources
218
- instance = resource_class.allocate
219
-
220
- # Set up the instance with parameters
221
- instance.instance_variable_set(:@params, params)
222
-
223
- # Initialize the instance (this calls the BaseResource initialize)
224
- instance.send(:initialize)
225
-
226
- # Call content to get the actual guide content
227
- instance.content
228
- end
229
-
230
- def generate_guide_name_variations(guide_name, guides_type)
231
- variations = []
232
-
233
- # Original name
234
- variations << guide_name
235
-
236
- # Underscore variations
237
- variations << guide_name.gsub(/[_-]/, "_")
238
- variations << guide_name.gsub(/\s+/, "_")
239
-
240
- # Hyphen variations
241
- variations << guide_name.gsub(/[_-]/, "-")
242
- variations << guide_name.gsub(/\s+/, "-")
243
-
244
- # Case variations
245
- variations << guide_name.downcase
246
- variations << guide_name.upcase
247
-
248
- # Remove special characters
249
- variations << guide_name.gsub(/[^a-zA-Z0-9_\/.-]/, "")
250
-
251
- # Common guide patterns (snake_case, kebab-case)
252
- if !guide_name.include?("_")
253
- variations << guide_name.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
254
- end
255
-
256
- # For Stimulus/Turbo, try with handbook/ and reference/ prefixes
257
- # Custom and Rails and Kamal guides use flat structure, so no prefixes needed
258
- unless guide_name.include?("/") || %w[custom rails kamal].include?(guides_type)
259
- variations << "handbook/#{guide_name}"
260
- variations << "reference/#{guide_name}"
261
- end
262
-
263
- # Remove path prefixes for alternatives (for Stimulus/Turbo)
264
- if guide_name.include?("/") && !%w[custom rails kamal].include?(guides_type)
265
- base_name = guide_name.split("/").last
266
- variations << base_name
267
- variations.concat(generate_guide_name_variations(base_name, guides_type))
268
- end
269
-
270
- variations.uniq.compact # rubocop:disable Performance/ChainArrayAllocation
271
- end
272
-
273
- def format_guide_not_found_message(guide_name, guides_type)
274
- message = <<~MSG
275
- # Guide Not Found
276
-
277
- Guide '#{guide_name}' not found in #{guides_type} guides.
278
-
279
- ## Suggestions:
280
- - Use `load_guide guides: "#{guides_type}"` to see all available guides
281
- - Check the guide name spelling
282
- - Try common variations like:
283
- - `#{guide_name.gsub(/[_-]/, "_")}`
284
- - `#{guide_name.gsub(/\s+/, "_")}`
285
- - `#{guide_name.downcase}`
286
- MSG
287
-
288
- # Add framework-specific suggestions
289
- case guides_type
290
- when "stimulus", "turbo"
291
- message += <<~MSG
292
- - Try with section prefix: `handbook/#{guide_name}` or `reference/#{guide_name}`
293
- - Try without section prefix if you used one
294
- MSG
295
- when "custom"
296
- message += <<~MSG
297
- - Import custom guides with: `rails-mcp-server-download-resources --file /path/to/guides`
298
- - Make sure your custom guides have been imported
299
- MSG
300
- when "kamal"
301
- message += <<~MSG
302
- - Try with section prefix: `commands/#{guide_name}` or `configuration/#{guide_name}`
303
- - Check available sections: installation, configuration, commands, hooks, upgrading
304
- MSG
305
- end
306
-
307
- message += <<~MSG
308
-
309
- ## Available Commands:
310
- - List guides: `load_guide guides: "#{guides_type}"`
311
- - Load guide: `load_guide guides: "#{guides_type}", guide: "guide_name"`
312
-
313
- ## Example Usage:
314
- ```
315
- MSG
316
-
317
- case guides_type
318
- when "rails"
319
- message += <<~MSG
320
- load_guide guides: "rails", guide: "active_record_validations"
321
- load_guide guides: "rails", guide: "getting_started"
322
- MSG
323
- when "stimulus"
324
- message += <<~MSG
325
- load_guide guides: "stimulus", guide: "actions"
326
- load_guide guides: "stimulus", guide: "01_introduction"
327
- load_guide guides: "stimulus", guide: "handbook/02_hello_stimulus"
328
- MSG
329
- when "turbo"
330
- message += <<~MSG
331
- load_guide guides: "turbo", guide: "drive"
332
- load_guide guides: "turbo", guide: "02_drive"
333
- load_guide guides: "turbo", guide: "reference/attributes"
334
- MSG
335
- when "kamal"
336
- message += <<~MSG
337
- load_guide guides: "kamal", guide: "installation"
338
- load_guide guides: "kamal", guide: "configuration"
339
- load_guide guides: "kamal", guide: "commands/deploy"
340
- MSG
341
- when "custom"
342
- message += <<~MSG
343
- load_guide guides: "custom", guide: "api_documentation"
344
- load_guide guides: "custom", guide: "setup_guide"
345
- load_guide guides: "custom", guide: "user_manual"
346
- MSG
347
- end
348
-
349
- message += "```\n"
350
-
351
- log(:warn, "Guide not found: #{guide_name}")
352
- message
353
- end
354
-
355
- def format_error_message(message)
356
- <<~MSG
357
- # Error Loading Guide
358
-
359
- #{message}
360
-
361
- ## Troubleshooting:
362
- - Ensure guides are downloaded: `rails-mcp-server-download-resources [rails|stimulus|turbo|kamal]`
363
- - For custom guides: `rails-mcp-server-download-resources --file /path/to/guides`
364
- - Check that the MCP server is properly configured
365
- - Verify guide name is correct
366
- - Use `load_guide guides: "[rails|stimulus|turbo|kamal|custom]"` to see available guides
367
- MSG
368
- end
369
- end
370
- end
@@ -1,86 +0,0 @@
1
- module RailsMcpServer
2
- class ProjectInfo < BaseTool
3
- tool_name "project_info"
4
-
5
- description "Retrieve comprehensive information about the current Rails project, including Rails version, directory structure, API-only status, and overall project organization. Useful for initial project exploration and understanding the codebase structure."
6
-
7
- def call
8
- unless current_project
9
- message = "No active project. Please switch to a project first."
10
- log(:warn, message)
11
-
12
- return message
13
- end
14
-
15
- # Get additional project information
16
- gemfile_path = File.join(active_project_path, "Gemfile")
17
- gemfile_content = File.exist?(gemfile_path) ? File.read(gemfile_path) : "Gemfile not found"
18
-
19
- # Get Rails version
20
- rails_version = gemfile_content.match(/gem ['"]rails['"],\s*['"](.+?)['"]/)&.captures&.first || "Unknown"
21
-
22
- # Check if it's an API-only app
23
- config_application_path = File.join(active_project_path, "config", "application.rb")
24
- is_api_only = File.exist?(config_application_path) &&
25
- File.read(config_application_path).include?("config.api_only = true")
26
-
27
- log(:info, "Project info: Rails v#{rails_version}, API-only: #{is_api_only}")
28
-
29
- <<~INFO
30
- Current project: #{current_project}
31
- Path: #{active_project_path}
32
- Rails version: #{rails_version}
33
- API only: #{is_api_only ? "Yes" : "No"}
34
-
35
- Project structure:
36
- #{get_directory_structure(active_project_path, max_depth: 2)}
37
- INFO
38
- end
39
-
40
- private
41
-
42
- # Utility functions for Rails operations
43
- def get_directory_structure(path, max_depth: 3, current_depth: 0, prefix: "")
44
- return "" if current_depth > max_depth || !File.directory?(path)
45
-
46
- # Define ignored directories
47
- ignored_dirs = [
48
- ".git", "node_modules", "tmp", "log",
49
- "storage", "coverage", "public/assets",
50
- "public/packs", ".bundle", "vendor/bundle",
51
- "vendor/cache", ".ruby-lsp"
52
- ]
53
-
54
- output = ""
55
- directories = []
56
- files = []
57
-
58
- Dir.foreach(path) do |entry|
59
- next if entry == "." || entry == ".."
60
- next if ignored_dirs.include?(entry) # Skip ignored directories
61
-
62
- full_path = File.join(path, entry)
63
-
64
- if File.directory?(full_path)
65
- directories << entry
66
- else
67
- files << entry
68
- end
69
- end
70
-
71
- directories.sort.each do |dir|
72
- output << "#{prefix}└── #{dir}/\n"
73
- full_path = File.join(path, dir)
74
- output << get_directory_structure(full_path, max_depth: max_depth,
75
- current_depth: current_depth + 1,
76
- prefix: "#{prefix} ")
77
- end
78
-
79
- files.sort.each do |file|
80
- output << "#{prefix}└── #{file}\n"
81
- end
82
-
83
- output
84
- end
85
- end
86
- end