railsforge 1.0.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 (73) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +528 -0
  4. data/bin/railsforge +8 -0
  5. data/lib/railsforge/analyzers/base_analyzer.rb +41 -0
  6. data/lib/railsforge/analyzers/controller_analyzer.rb +83 -0
  7. data/lib/railsforge/analyzers/database_analyzer.rb +55 -0
  8. data/lib/railsforge/analyzers/metrics_analyzer.rb +55 -0
  9. data/lib/railsforge/analyzers/model_analyzer.rb +74 -0
  10. data/lib/railsforge/analyzers/performance_analyzer.rb +161 -0
  11. data/lib/railsforge/analyzers/refactor_analyzer.rb +118 -0
  12. data/lib/railsforge/analyzers/security_analyzer.rb +169 -0
  13. data/lib/railsforge/analyzers/spec_analyzer.rb +58 -0
  14. data/lib/railsforge/api_generator.rb +397 -0
  15. data/lib/railsforge/audit.rb +289 -0
  16. data/lib/railsforge/cli.rb +671 -0
  17. data/lib/railsforge/config.rb +181 -0
  18. data/lib/railsforge/database_analyzer.rb +300 -0
  19. data/lib/railsforge/doctor.rb +250 -0
  20. data/lib/railsforge/feature_generator.rb +560 -0
  21. data/lib/railsforge/generator.rb +313 -0
  22. data/lib/railsforge/generators/base_generator.rb +70 -0
  23. data/lib/railsforge/generators/demo_generator.rb +307 -0
  24. data/lib/railsforge/generators/devops_generator.rb +287 -0
  25. data/lib/railsforge/generators/monitoring_generator.rb +134 -0
  26. data/lib/railsforge/generators/service_generator.rb +122 -0
  27. data/lib/railsforge/generators/stimulus_controller_generator.rb +129 -0
  28. data/lib/railsforge/generators/test_generator.rb +289 -0
  29. data/lib/railsforge/generators/view_component_generator.rb +169 -0
  30. data/lib/railsforge/graph.rb +270 -0
  31. data/lib/railsforge/loader.rb +56 -0
  32. data/lib/railsforge/mailer_generator.rb +191 -0
  33. data/lib/railsforge/plugins/plugin_loader.rb +60 -0
  34. data/lib/railsforge/plugins.rb +30 -0
  35. data/lib/railsforge/profiles/admin_app.yml +49 -0
  36. data/lib/railsforge/profiles/api_only.yml +47 -0
  37. data/lib/railsforge/profiles/blog.yml +47 -0
  38. data/lib/railsforge/profiles/standard.yml +44 -0
  39. data/lib/railsforge/profiles.rb +99 -0
  40. data/lib/railsforge/refactor_analyzer.rb +401 -0
  41. data/lib/railsforge/refactor_controller.rb +277 -0
  42. data/lib/railsforge/refactors/refactor_controller.rb +117 -0
  43. data/lib/railsforge/template_loader.rb +105 -0
  44. data/lib/railsforge/templates/v1/form/spec_template.rb +18 -0
  45. data/lib/railsforge/templates/v1/form/template.rb +28 -0
  46. data/lib/railsforge/templates/v1/job/spec_template.rb +17 -0
  47. data/lib/railsforge/templates/v1/job/template.rb +13 -0
  48. data/lib/railsforge/templates/v1/policy/spec_template.rb +41 -0
  49. data/lib/railsforge/templates/v1/policy/template.rb +57 -0
  50. data/lib/railsforge/templates/v1/presenter/spec_template.rb +12 -0
  51. data/lib/railsforge/templates/v1/presenter/template.rb +13 -0
  52. data/lib/railsforge/templates/v1/query/spec_template.rb +12 -0
  53. data/lib/railsforge/templates/v1/query/template.rb +16 -0
  54. data/lib/railsforge/templates/v1/serializer/spec_template.rb +13 -0
  55. data/lib/railsforge/templates/v1/serializer/template.rb +11 -0
  56. data/lib/railsforge/templates/v1/service/spec_template.rb +12 -0
  57. data/lib/railsforge/templates/v1/service/template.rb +25 -0
  58. data/lib/railsforge/templates/v1/stimulus_controller/template.rb +35 -0
  59. data/lib/railsforge/templates/v1/view_component/template.rb +24 -0
  60. data/lib/railsforge/templates/v2/job/template.rb +49 -0
  61. data/lib/railsforge/templates/v2/query/template.rb +66 -0
  62. data/lib/railsforge/templates/v2/service/spec_template.rb +33 -0
  63. data/lib/railsforge/templates/v2/service/template.rb +71 -0
  64. data/lib/railsforge/templates/v3/job/template.rb +72 -0
  65. data/lib/railsforge/templates/v3/query/spec_template.rb +54 -0
  66. data/lib/railsforge/templates/v3/query/template.rb +115 -0
  67. data/lib/railsforge/templates/v3/service/spec_template.rb +51 -0
  68. data/lib/railsforge/templates/v3/service/template.rb +84 -0
  69. data/lib/railsforge/version.rb +5 -0
  70. data/lib/railsforge/wizard.rb +265 -0
  71. data/lib/railsforge/wizard_tui.rb +286 -0
  72. data/lib/railsforge.rb +13 -0
  73. metadata +216 -0
@@ -0,0 +1,47 @@
1
+ # Blog Profile Configuration
2
+ # Use with: railsforge new my_blog --profile=blog
3
+
4
+ name: blog
5
+ description: A standard blog application with articles, comments, and user authentication
6
+
7
+ # Default folders to create
8
+ folders:
9
+ - app/controllers
10
+ - app/models
11
+ - app/views
12
+ - app/helpers
13
+ - app/mailers
14
+ - app/jobs
15
+ - app/services
16
+ - app/queries
17
+ - app/presenters
18
+ - app/policies
19
+ - app/serializers
20
+ - app/forms
21
+ - config/initializers
22
+ - db/migrate
23
+ - spec/models
24
+ - spec/controllers
25
+ - spec/views
26
+ - spec/requests
27
+ - spec/services
28
+ - spec/policies
29
+ - spec/serializers
30
+
31
+ # Default generators to run after app creation
32
+ generators:
33
+ - model
34
+ - scaffold
35
+ - resource
36
+
37
+ # Features to enable by default
38
+ features:
39
+ - authentication
40
+ - authorization
41
+ - mailers
42
+
43
+ # Template variables
44
+ defaults:
45
+ css_framework: tailwind
46
+ testing: rspec
47
+ database: postgresql
@@ -0,0 +1,44 @@
1
+ # Standard Profile Configuration
2
+ # Use with: railsforge new my_app --profile=standard (default)
3
+
4
+ name: standard
5
+ description: A standard Rails application with common defaults
6
+
7
+ # Default folders to create
8
+ folders:
9
+ - app/controllers
10
+ - app/models
11
+ - app/views
12
+ - app/helpers
13
+ - app/mailers
14
+ - app/jobs
15
+ - app/services
16
+ - app/queries
17
+ - app/presenters
18
+ - app/policies
19
+ - app/serializers
20
+ - app/forms
21
+ - config/initializers
22
+ - db/migrate
23
+ - spec/models
24
+ - spec/controllers
25
+ - spec/requests
26
+ - spec/services
27
+ - spec/policies
28
+
29
+ # Default generators to run after app creation
30
+ generators:
31
+ - model
32
+ - scaffold
33
+ - resource
34
+
35
+ # Features to enable by default
36
+ features:
37
+ - authentication
38
+ - mailers
39
+
40
+ # Template variables
41
+ defaults:
42
+ css_framework: tailwind
43
+ testing: rspec
44
+ database: postgresql
@@ -0,0 +1,99 @@
1
+ # ProfileLoader module handles loading RailsForge profiles
2
+ require 'yaml'
3
+
4
+ module RailsForge
5
+ # Profile module for managing application profiles
6
+ module Profile
7
+ # Error class for profile loading issues
8
+ class ProfileError < StandardError; end
9
+
10
+ # Profile directory path
11
+ PROFILES_DIR = File.expand_path('../profiles', __FILE__)
12
+
13
+ # Load a profile by name
14
+ # @param profile_name [String] The name of the profile to load
15
+ # @return [Hash] The profile configuration
16
+ # @raises [ProfileError] If profile not found or invalid
17
+ def self.load(profile_name)
18
+ profile_path = File.join(PROFILES_DIR, "#{profile_name}.yml")
19
+
20
+ unless File.exist?(profile_path)
21
+ raise ProfileError, "Profile '#{profile_name}' not found. Available profiles: #{available_profiles.join(', ')}"
22
+ end
23
+
24
+ begin
25
+ YAML.safe_load(File.read(profile_path), permitted_classes: [], permitted_symbols: [], aliases: true)
26
+ rescue => e
27
+ raise ProfileError, "Failed to load profile '#{profile_name}': #{e.message}"
28
+ end
29
+ end
30
+
31
+ # Get list of available profiles
32
+ # @return [Array<String>] Array of profile names
33
+ def self.available_profiles
34
+ return [] unless Dir.exist?(PROFILES_DIR)
35
+
36
+ Dir.glob(File.join(PROFILES_DIR, '*.yml')).map do |file|
37
+ File.basename(file, '.yml')
38
+ end.sort
39
+ end
40
+
41
+ # Validate a profile configuration
42
+ # @param profile [Hash] The profile configuration to validate
43
+ # @return [Boolean] True if valid
44
+ def self.validate?(profile)
45
+ return false unless profile.is_a?(Hash)
46
+
47
+ required_keys = %w[name description folders]
48
+ required_keys.all? { |key| profile.key?(key) }
49
+ end
50
+
51
+ # Create folders defined in profile
52
+ # @param base_path [String] The Rails app root path
53
+ # @param profile [Hash] The profile configuration
54
+ def self.create_folders(base_path, profile)
55
+ folders = profile['folders'] || []
56
+
57
+ folders.each do |folder|
58
+ full_path = File.join(base_path, folder)
59
+ unless Dir.exist?(full_path)
60
+ FileUtils.mkdir_p(full_path)
61
+ puts " Created #{folder}"
62
+ end
63
+ end
64
+ end
65
+
66
+ # Get default features from profile
67
+ # @param profile [Hash] The profile configuration
68
+ # @return [Array<String>] Array of feature names
69
+ def self.default_features(profile)
70
+ profile['features'] || []
71
+ end
72
+
73
+ # Get default template variables from profile
74
+ # @param profile [Hash] The profile configuration
75
+ # @return [Hash] Default template variables
76
+ def self.defaults(profile)
77
+ profile['defaults'] || {}
78
+ end
79
+
80
+ # Show available profiles with descriptions
81
+ # @return [String] Formatted list of profiles
82
+ def self.show_available
83
+ profiles = available_profiles
84
+
85
+ if profiles.empty?
86
+ return "No profiles available."
87
+ end
88
+
89
+ output = "Available profiles:\n\n"
90
+
91
+ profiles.each do |name|
92
+ profile = load(name)
93
+ output += " #{name.ljust(15)} - #{profile['description']}\n"
94
+ end
95
+
96
+ output
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,401 @@
1
+ module RailsForge
2
+ # RefactorAnalyzer module handles refactoring suggestions and code extraction
3
+ module RefactorAnalyzer
4
+ # Error class for refactoring issues
5
+ class RefactorError < StandardError; end
6
+
7
+ # Configuration thresholds
8
+ CONTROLLER_MAX_LINES = 150
9
+ CONTROLLER_MAX_METHODS = 10
10
+ MODEL_MAX_LINES = 200
11
+ MODEL_MAX_METHOD_LINES = 15
12
+
13
+ # Analyzes controllers for refactoring opportunities
14
+ # @param base_path [String] Rails app root path
15
+ # @return [Array<Hash>] Array of refactoring suggestions
16
+ def self.analyze_controllers(base_path = nil)
17
+ base_path ||= find_rails_app_path
18
+ raise RefactorError, "Not in a Rails application directory" unless base_path
19
+
20
+ controllers_dir = File.join(base_path, "app", "controllers")
21
+ return [] unless Dir.exist?(controllers_dir)
22
+
23
+ results = []
24
+ Dir.glob(File.join(controllers_dir, "**", "*_controller.rb")).each do |file|
25
+ result = analyze_controller_file(file)
26
+ results << result if result[:needs_refactoring]
27
+ end
28
+ results
29
+ end
30
+
31
+ # Analyzes models for refactoring opportunities
32
+ # @param base_path [String] Rails app root path
33
+ # @return [Array<Hash>] Array of refactoring suggestions
34
+ def self.analyze_models(base_path = nil)
35
+ base_path ||= find_rails_app_path
36
+ raise RefactorError, "Not in a Rails application directory" unless base_path
37
+
38
+ models_dir = File.join(base_path, "app", "models")
39
+ return [] unless Dir.exist?(models_dir)
40
+
41
+ results = []
42
+ Dir.glob(File.join(models_dir, "**", "*.rb")).each do |file|
43
+ next if file.end_with?("_application.rb")
44
+ result = analyze_model_file(file)
45
+ results << result if result[:needs_refactoring]
46
+ end
47
+ results
48
+ end
49
+
50
+ # Analyzes a controller file
51
+ # @param file_path [String] Path to controller file
52
+ # @return [Hash] Analysis results
53
+ def self.analyze_controller_file(file_path)
54
+ content = File.read(file_path)
55
+ lines = content.lines.count
56
+ methods = extract_methods(content)
57
+
58
+ issues = []
59
+ suggestions = []
60
+
61
+ if lines > CONTROLLER_MAX_LINES
62
+ issues << "Controller exceeds #{CONTROLLER_MAX_LINES} lines (currently #{lines})"
63
+ suggestions << "Consider moving business logic into a Service object"
64
+ end
65
+
66
+ if methods.count > CONTROLLER_MAX_METHODS
67
+ issues << "Controller has #{methods.count} methods (recommended: #{CONTROLLER_MAX_METHODS} or less)"
68
+ suggestions << "Consider extracting some actions into separate controllers or using a Service"
69
+ end
70
+
71
+ # Find long methods that could be extracted
72
+ methods.each do |method|
73
+ if method[:lines] > MODEL_MAX_METHOD_LINES
74
+ suggestions << "Method `#{method[:name]}` has #{method[:lines]} lines - consider extracting to Service"
75
+ end
76
+ end
77
+
78
+ {
79
+ type: :controller,
80
+ file: File.basename(file_path),
81
+ path: file_path,
82
+ lines: lines,
83
+ methods: methods,
84
+ issues: issues,
85
+ suggestions: suggestions,
86
+ needs_refactoring: issues.any? || suggestions.any?
87
+ }
88
+ end
89
+
90
+ # Analyzes a model file
91
+ # @param file_path [String] Path to model file
92
+ # @return [Hash] Analysis results
93
+ def self.analyze_model_file(file_path)
94
+ content = File.read(file_path)
95
+ lines = content.lines.count
96
+ methods = extract_methods(content)
97
+
98
+ issues = []
99
+ suggestions = []
100
+
101
+ if lines > MODEL_MAX_LINES
102
+ issues << "Model exceeds #{MODEL_MAX_LINES} lines (currently #{lines})"
103
+ suggestions << "Consider extracting scopes into Query objects or validations to a Form"
104
+ end
105
+
106
+ # Find long methods
107
+ methods.each do |method|
108
+ if method[:lines] > MODEL_MAX_METHOD_LINES
109
+ suggestions << "Method `#{method[:name]}` has #{method[:lines]} lines - consider extracting to a Service"
110
+ end
111
+ end
112
+
113
+ {
114
+ type: :model,
115
+ file: File.basename(file_path),
116
+ path: file_path,
117
+ lines: lines,
118
+ methods: methods,
119
+ issues: issues,
120
+ suggestions: suggestions,
121
+ needs_refactoring: issues.any? || suggestions.any?
122
+ }
123
+ end
124
+
125
+ # Extracts method names and line counts from content
126
+ # @param content [String] Ruby code content
127
+ # @return [Array<Hash>] Array of method info
128
+ def self.extract_methods(content)
129
+ methods = []
130
+
131
+ # Match def method_name or def self.method_name
132
+ content.scan(/def\s+(self\.)?([a-z_][a-zA-Z_]*)/) do |prefix, name|
133
+ methods << {
134
+ name: name,
135
+ is_class_method: prefix == "self.",
136
+ lines: 1 # Simplified - just mark as present
137
+ }
138
+ end
139
+
140
+ methods
141
+ end
142
+
143
+ # Counts lines in a method
144
+ # @param content [String] Ruby code content
145
+ # @param method_name [String] Method name
146
+ # @param start_line [Integer] Starting line number
147
+ # @return [Integer] Number of lines
148
+ def self.count_method_lines(content, method_name, start_line)
149
+ # Find the end of the method
150
+ lines = content.lines
151
+ end_pos = content.length
152
+
153
+ # Look for next def or class or end
154
+ rest = content.lines[start_line..-1].join
155
+ if rest =~ /\n\s*def\s+(self\.)?[a-z_]/i
156
+ end_pos = $~.begin(0)
157
+ elsif rest =~ /\n\s*(class|module)\s+/
158
+ end_pos = $~.begin(0)
159
+ elsif rest =~ /\n\s*end\s*$/
160
+ end_pos = $~.begin(0)
161
+ end
162
+
163
+ content[start_line..end_pos].lines.count
164
+ end
165
+
166
+ # Generates a service file for extracted logic
167
+ # @param name [String] Service name
168
+ # @param logic [String] Logic to extract
169
+ # @param base_path [String] Rails app root
170
+ # @return [String] Path to created file
171
+ def self.generate_service(name, logic, base_path = nil)
172
+ base_path ||= find_rails_app_path
173
+ raise RefactorError, "Not in a Rails application directory" unless base_path
174
+
175
+ service_dir = File.join(base_path, "app", "services")
176
+ FileUtils.mkdir_p(service_dir)
177
+
178
+ file_name = "#{name.underscore}_service.rb"
179
+ file_path = File.join(service_dir, file_name)
180
+
181
+ if File.exist?(file_path)
182
+ puts " Skipping service (already exists)"
183
+ return file_path
184
+ end
185
+
186
+ content = <<~RUBY
187
+ # Service class for #{name}
188
+ # Extracted from controller/model logic
189
+ #
190
+ # Usage:
191
+ # #{name}Service.call(params)
192
+ class #{name}Service
193
+ def initialize(**args)
194
+ @args = args
195
+ end
196
+
197
+ def call
198
+ # Extracted logic:
199
+ # #{logic.gsub("\n", "\n # ")}
200
+ end
201
+ end
202
+ RUBY
203
+
204
+ File.write(file_path, content)
205
+ puts " Created app/services/#{file_name}"
206
+ file_path
207
+ end
208
+
209
+ # Generates a query file for extracted logic
210
+ # @param name [String] Query name
211
+ # @param scope [String] Scope logic
212
+ # @param base_path [String] Rails app root
213
+ # @return [String] Path to created file
214
+ def self.generate_query(name, scope, base_path = nil)
215
+ base_path ||= find_rails_app_path
216
+ raise RefactorError, "Not in a Rails application directory" unless base_path
217
+
218
+ query_dir = File.join(base_path, "app", "queries")
219
+ FileUtils.mkdir_p(query_dir)
220
+
221
+ file_name = "find_#{name.underscore}.rb"
222
+ file_path = File.join(query_dir, file_name)
223
+
224
+ if File.exist?(file_path)
225
+ puts " Skipping query (already exists)"
226
+ return file_path
227
+ end
228
+
229
+ content = <<~RUBY
230
+ # Query class for #{name}
231
+ # Extracted scope/query logic
232
+ #
233
+ # Usage:
234
+ # Find#{name}.call
235
+ class Find#{name}
236
+ def initialize(scope: nil)
237
+ @scope = scope || #{name}.all
238
+ end
239
+
240
+ def call
241
+ # Extracted scope:
242
+ # #{scope.gsub("\n", "\n # ")}
243
+ @scope
244
+ end
245
+ end
246
+ RUBY
247
+
248
+ File.write(file_path, content)
249
+ puts " Created app/queries/#{file_name}"
250
+ file_path
251
+ end
252
+
253
+ # Generates an RSpec test for a service
254
+ # @param name [String] Service name
255
+ # @param base_path [String] Rails app root
256
+ # @return [String] Path to created file
257
+ def self.generate_service_spec(name, base_path = nil)
258
+ base_path ||= find_rails_app_path
259
+ raise RefactorError, "Not in a Rails application directory" unless base_path
260
+
261
+ spec_dir = File.join(base_path, "spec", "services")
262
+ FileUtils.mkdir_p(spec_dir)
263
+
264
+ file_name = "#{name.underscore}_service_spec.rb"
265
+ file_path = File.join(spec_dir, file_name)
266
+
267
+ if File.exist?(file_path)
268
+ puts " Skipping spec (already exists)"
269
+ return file_path
270
+ end
271
+
272
+ content = <<~RUBY
273
+ require 'rails_helper'
274
+
275
+ RSpec.describe #{name}Service do
276
+ let(:params) { {} }
277
+ subject { described_class.new(params) }
278
+
279
+ describe '#call' do
280
+ it 'returns successful result' do
281
+ expect(subject.call).to be_truthy
282
+ end
283
+ end
284
+ end
285
+ RUBY
286
+
287
+ File.write(file_path, content)
288
+ puts " Created spec/services/#{file_name}"
289
+ file_path
290
+ end
291
+
292
+ # Generates an RSpec test for a query
293
+ # @param name [String] Query name
294
+ # @param base_path [String] Rails app root
295
+ # @return [String] Path to created file
296
+ def self.generate_query_spec(name, base_path = nil)
297
+ base_path ||= find_rails_app_path
298
+ raise RefactorError, "Not in a Rails application directory" unless base_path
299
+
300
+ spec_dir = File.join(base_path, "spec", "queries")
301
+ FileUtils.mkdir_p(spec_dir)
302
+
303
+ file_name = "find_#{name.underscore}_spec.rb"
304
+ file_path = File.join(spec_dir, file_name)
305
+
306
+ if File.exist?(file_path)
307
+ puts " Skipping spec (already exists)"
308
+ return file_path
309
+ end
310
+
311
+ content = <<~RUBY
312
+ require 'rails_helper'
313
+
314
+ RSpec.describe Find#{name} do
315
+ let(:scope) { #{name}.all }
316
+ subject { described_class.new(scope: scope) }
317
+
318
+ describe '#call' do
319
+ it 'returns scope' do
320
+ expect(subject.call).to eq(scope)
321
+ end
322
+ end
323
+ end
324
+ RUBY
325
+
326
+ File.write(file_path, content)
327
+ puts " Created spec/queries/#{file_name}"
328
+ file_path
329
+ end
330
+
331
+ # Extracts code from a controller/model and creates a service
332
+ # @param file_path [String] Source file path
333
+ # @param method_names [Array<String>] Methods to extract
334
+ # @param service_name [String] Name for new service
335
+ # @return [Hash] Results
336
+ def self.extract_to_service(file_path, method_names, service_name)
337
+ content = File.read(file_path)
338
+
339
+ extracted_logic = []
340
+ method_names.each do |method_name|
341
+ # Find method in content
342
+ if content.include?("def #{method_name}")
343
+ # Extract method and its body
344
+ method_match = content.match(/def #{method_name}.*?(\n\s*end\n)/m)
345
+ extracted_logic << method_match[0] if method_match
346
+ end
347
+ end
348
+
349
+ base_path = find_rails_app_path
350
+ generate_service(service_name, extracted_logic.join("\n"), base_path)
351
+ generate_service_spec(service_name, base_path)
352
+
353
+ {
354
+ service: "app/services/#{service_name.underscore}_service.rb",
355
+ spec: "spec/services/#{service_name.underscore}_service_spec.rb"
356
+ }
357
+ end
358
+
359
+ # Prints refactoring report
360
+ # @param results [Array<Hash>] Analysis results
361
+ def self.print_report(results)
362
+ return puts "\n✓ No refactoring needed!" if results.empty?
363
+
364
+ puts "\n" + "=" * 60
365
+ puts "REFACTORING REPORT"
366
+ puts "=" * 60
367
+
368
+ results.each do |result|
369
+ puts "\n📁 #{result[:file]} (#{result[:type]})"
370
+ puts " Lines: #{result[:lines]}"
371
+
372
+ if result[:issues].any?
373
+ puts "\n ⚠️ Issues:"
374
+ result[:issues].each { |issue| puts " • #{issue}" }
375
+ end
376
+
377
+ if result[:suggestions].any?
378
+ puts "\n 💡 Suggestions:"
379
+ result[:suggestions].each { |sug| puts " • #{sug}" }
380
+ end
381
+ end
382
+
383
+ puts "\n" + "=" * 60
384
+ puts "Total files needing refactoring: #{results.count}"
385
+ puts "=" * 60
386
+ end
387
+
388
+ # Finds Rails app root path
389
+ # @return [String, nil] Rails app path
390
+ def self.find_rails_app_path
391
+ path = Dir.pwd
392
+ 10.times do
393
+ return path if File.exist?(File.join(path, "config", "application.rb"))
394
+ parent = File.dirname(path)
395
+ break if parent == path
396
+ path = parent
397
+ end
398
+ nil
399
+ end
400
+ end
401
+ end