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,397 @@
1
+ module RailsForge
2
+ # ApiGenerator module handles generating API resources
3
+ module ApiGenerator
4
+ # Error class for invalid resource names
5
+ class InvalidResourceNameError < StandardError; end
6
+
7
+ # Simple pluralize helper
8
+ def self.pluralize(word)
9
+ return word + 's' unless word.end_with?('s')
10
+ return word + 'es' if word.end_with?('sh') || word.end_with?('ch')
11
+ word + 's'
12
+ end
13
+
14
+ # Simple underscore helper
15
+ def self.underscore(word)
16
+ word.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
17
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
18
+ .downcase
19
+ end
20
+
21
+ # Validates the resource name
22
+ def self.validate_resource_name(name)
23
+ if name.nil? || name.strip.empty?
24
+ raise InvalidResourceNameError, "Resource name cannot be empty"
25
+ end
26
+
27
+ unless name =~ /\A[A-Z][a-zA-Z0-9]*\z/
28
+ raise InvalidResourceNameError, "Resource name must be in PascalCase (e.g., User)"
29
+ end
30
+ end
31
+
32
+ # Generates an API resource with all necessary files
33
+ def self.generate(resource_name, with_spec: true, version: "v1", namespace: "api")
34
+ validate_resource_name(resource_name)
35
+
36
+ base_path = find_rails_app_path
37
+ raise InvalidResourceNameError, "Not in a Rails application directory" unless base_path
38
+
39
+ results = []
40
+
41
+ # Generate directories
42
+ controller_dir = File.join(base_path, "app", "controllers", namespace, version)
43
+ FileUtils.mkdir_p(controller_dir)
44
+
45
+ serializer_dir = File.join(base_path, "app", "serializers")
46
+ FileUtils.mkdir_p(serializer_dir)
47
+
48
+ policy_dir = File.join(base_path, "app", "policies")
49
+ FileUtils.mkdir_p(policy_dir)
50
+
51
+ service_dir = File.join(base_path, "app", "services")
52
+ FileUtils.mkdir_p(service_dir)
53
+
54
+ query_dir = File.join(base_path, "app", "queries")
55
+ FileUtils.mkdir_p(query_dir)
56
+
57
+ results << generate_controller(base_path, resource_name, namespace, version)
58
+ results << generate_serializer(base_path, resource_name)
59
+ results << generate_policy(base_path, resource_name)
60
+ results << generate_service(base_path, resource_name)
61
+ results << generate_query(base_path, resource_name)
62
+
63
+ if with_spec
64
+ spec_dir = File.join(base_path, "spec", "requests", namespace, version)
65
+ FileUtils.mkdir_p(spec_dir)
66
+ results << generate_request_spec(base_path, resource_name, namespace, version)
67
+ end
68
+
69
+ "API resource '#{resource_name}' generated successfully with #{results.count} files!"
70
+ end
71
+
72
+ def self.generate_controller(base_path, resource_name, namespace, version)
73
+ resource_plural = pluralize(resource_name)
74
+ resource_underscore = underscore(resource_name)
75
+ resource_underscore_plural = pluralize(resource_underscore)
76
+
77
+ file_name = "#{resource_underscore}_controller.rb"
78
+ file_path = File.join(base_path, "app", "controllers", namespace, version, file_name)
79
+
80
+ return " Skipping controller (already exists)" if File.exist?(file_path)
81
+
82
+ content = <<~RUBY
83
+ # API Controller for #{resource_name}
84
+ # Version: #{version}
85
+ # Namespace: #{namespace}
86
+ #
87
+ # Generates standard CRUD actions for REST API
88
+ class #{resource_plural}Controller < ApplicationController
89
+ before_action :set_#{resource_underscore}, only: [:show, :update, :destroy]
90
+ before_action :authenticate_user!, unless: :devise_controller?
91
+
92
+ # GET /#{resource_underscore_plural}
93
+ def index
94
+ @#{resource_underscore_plural} = #{resource_name}Query.call
95
+ render json: #{resource_name}Serializer.new(@#{resource_underscore_plural}).serializable_hash
96
+ end
97
+
98
+ # GET /#{resource_underscore_plural}/:id
99
+ def show
100
+ render json: #{resource_name}Serializer.new(@#{resource_underscore}).serializable_hash
101
+ end
102
+
103
+ # POST /#{resource_underscore_plural}
104
+ def create
105
+ @result = Create#{resource_name}Service.call(#{resource_underscore}_params)
106
+
107
+ if @result.success?
108
+ render json: #{resource_name}Serializer.new(@result.#{resource_underscore}).serializable_hash, status: :created
109
+ else
110
+ render json: { errors: @result.errors }, status: :unprocessable_entity
111
+ end
112
+ end
113
+
114
+ # PATCH/PUT /#{resource_underscore_plural}/:id
115
+ def update
116
+ @result = Update#{resource_name}Service.call(@#{resource_underscore}, #{resource_underscore}_params)
117
+
118
+ if @result.success?
119
+ render json: #{resource_name}Serializer.new(@result.#{resource_underscore}).serializable_hash
120
+ else
121
+ render json: { errors: @result.errors }, status: :unprocessable_entity
122
+ end
123
+ end
124
+
125
+ # DELETE /#{resource_underscore_plural}/:id
126
+ def destroy
127
+ @#{resource_underscore}.destroy
128
+ head :no_content
129
+ end
130
+
131
+ private
132
+
133
+ def set_#{resource_underscore}
134
+ @#{resource_underscore} = #{resource_name}.find(params[:id])
135
+ end
136
+
137
+ def #{resource_underscore}_params
138
+ params.require(:#{resource_underscore}).permit(:name)
139
+ end
140
+ end
141
+ RUBY
142
+
143
+ File.write(file_path, content)
144
+ puts " Created app/controllers/#{namespace}/#{version}/#{file_name}"
145
+ file_path
146
+ end
147
+
148
+ def self.generate_serializer(base_path, resource_name)
149
+ file_name = "#{underscore(resource_name)}_serializer.rb"
150
+ file_path = File.join(base_path, "app", "serializers", file_name)
151
+
152
+ return " Skipping serializer (already exists)" if File.exist?(file_path)
153
+
154
+ content = <<~RUBY
155
+ # Serializer for #{resource_name}
156
+ class #{resource_name}Serializer < ApplicationSerializer
157
+ attributes :id, :name, :created_at, :updated_at
158
+ end
159
+ RUBY
160
+
161
+ File.write(file_path, content)
162
+ puts " Created app/serializers/#{file_name}"
163
+ file_path
164
+ end
165
+
166
+ def self.generate_policy(base_path, resource_name)
167
+ file_name = "#{underscore(resource_name)}_policy.rb"
168
+ file_path = File.join(base_path, "app", "policies", file_name)
169
+
170
+ return " Skipping policy (already exists)" if File.exist?(file_path)
171
+
172
+ content = <<~RUBY
173
+ # Policy for #{resource_name}
174
+ class #{resource_name}Policy
175
+ attr_reader :user, :record
176
+
177
+ def initialize(user, record)
178
+ @user = user
179
+ @record = record
180
+ end
181
+
182
+ def index?
183
+ true
184
+ end
185
+
186
+ def show?
187
+ true
188
+ end
189
+
190
+ def create?
191
+ user.present?
192
+ end
193
+
194
+ def update?
195
+ user.present?
196
+ end
197
+
198
+ def destroy?
199
+ user.present?
200
+ end
201
+
202
+ class Scope
203
+ def initialize(user, scope)
204
+ @user = user
205
+ @scope = scope
206
+ end
207
+
208
+ def resolve
209
+ scope.all
210
+ end
211
+ end
212
+ end
213
+ RUBY
214
+
215
+ File.write(file_path, content)
216
+ puts " Created app/policies/#{file_name}"
217
+ file_path
218
+ end
219
+
220
+ def self.generate_service(base_path, resource_name)
221
+ res_underscore = underscore(resource_name)
222
+
223
+ # Create service
224
+ file_name = "create_#{res_underscore}_service.rb"
225
+ file_path = File.join(base_path, "app", "services", file_name)
226
+
227
+ unless File.exist?(file_path)
228
+ content = <<~RUBY
229
+ # Service for creating #{resource_name}
230
+ class Create#{resource_name}Service
231
+ def initialize(params)
232
+ @params = params
233
+ end
234
+
235
+ def call
236
+ @#{res_underscore} = #{resource_name}.create!(@params)
237
+ Result.new(success: true, #{res_underscore}: @#{res_underscore})
238
+ rescue => e
239
+ Result.new(success: false, errors: e.message)
240
+ end
241
+
242
+ class Result
243
+ attr_reader :#{res_underscore}, :errors
244
+ def initialize(success:, #{res_underscore}: nil, errors: nil)
245
+ @success = success
246
+ @#{res_underscore} = #{res_underscore}
247
+ @errors = errors
248
+ end
249
+
250
+ def success?
251
+ @success
252
+ end
253
+ end
254
+ end
255
+ RUBY
256
+
257
+ File.write(file_path, content)
258
+ puts " Created app/services/#{file_name}"
259
+ end
260
+
261
+ # Update service
262
+ file_name = "update_#{res_underscore}_service.rb"
263
+ file_path = File.join(base_path, "app", "services", file_name)
264
+
265
+ unless File.exist?(file_path)
266
+ content = <<~RUBY
267
+ # Service for updating #{resource_name}
268
+ class Update#{resource_name}Service
269
+ def initialize(#{res_underscore}, params)
270
+ @#{res_underscore} = #{res_underscore}
271
+ @params = params
272
+ end
273
+
274
+ def call
275
+ if @#{res_underscore}.update(@params)
276
+ Result.new(success: true, #{res_underscore}: @#{res_underscore})
277
+ else
278
+ Result.new(success: false, errors: @#{res_underscore}.errors.full_messages)
279
+ end
280
+ end
281
+
282
+ class Result
283
+ attr_reader :#{res_underscore}, :errors
284
+ def initialize(success:, #{res_underscore}: nil, errors: nil)
285
+ @success = success
286
+ @#{res_underscore} = #{res_underscore}
287
+ @errors = errors
288
+ end
289
+
290
+ def success?
291
+ @success
292
+ end
293
+ end
294
+ end
295
+ RUBY
296
+
297
+ File.write(file_path, content)
298
+ puts " Created app/services/#{file_name}"
299
+ end
300
+
301
+ "services"
302
+ end
303
+
304
+ def self.generate_query(base_path, resource_name)
305
+ file_name = "find_#{underscore(resource_name)}.rb"
306
+ file_path = File.join(base_path, "app", "queries", file_name)
307
+
308
+ return " Skipping query (already exists)" if File.exist?(file_path)
309
+
310
+ content = <<~RUBY
311
+ # Query for finding #{resource_name}
312
+ class Find#{resource_name}
313
+ def initialize(scope: nil)
314
+ @scope = scope || #{resource_name}.all
315
+ end
316
+
317
+ def call
318
+ @scope
319
+ end
320
+ end
321
+ RUBY
322
+
323
+ File.write(file_path, content)
324
+ puts " Created app/queries/#{file_name}"
325
+ file_path
326
+ end
327
+
328
+ def self.generate_request_spec(base_path, resource_name, namespace, version)
329
+ resource_plural = pluralize(resource_name)
330
+ resource_underscore = underscore(resource_name)
331
+
332
+ file_name = "#{resource_underscore}_spec.rb"
333
+ file_path = File.join(base_path, "spec", "requests", namespace, version, file_name)
334
+
335
+ return " Skipping spec (already exists)" if File.exist?(file_path)
336
+
337
+ content = <<~RUBY
338
+ require 'rails_helper'
339
+
340
+ RSpec.describe "#{namespace.capitalize}::#{version.capitalize}::#{resource_plural}Controller" do
341
+ let(:user) { User.create!(name: "Test", email: "test@example.com") }
342
+ let(:#{resource_underscore}) { #{resource_name}.create!(name: "Test #{resource_name}") }
343
+
344
+ before do
345
+ sign_in user
346
+ end
347
+
348
+ describe "GET /index" do
349
+ it "returns a success response" do
350
+ get :index
351
+ expect(response).to be_successful
352
+ end
353
+ end
354
+
355
+ describe "GET /show" do
356
+ it "returns a success response" do
357
+ get :show, params: { id: #{resource_underscore}.id }
358
+ expect(response).to be_successful
359
+ end
360
+ end
361
+
362
+ describe "POST /create" do
363
+ it "creates a new #{resource_underscore}" do
364
+ expect {
365
+ post :create, params: { #{resource_underscore}: { name: "New #{resource_name}" } }
366
+ }.to change(#{resource_name}, :count).by(1)
367
+ end
368
+ end
369
+
370
+ describe "DELETE /destroy" do
371
+ it "deletes the #{resource_underscore}" do
372
+ #{resource_underscore}
373
+ expect {
374
+ delete :destroy, params: { id: #{resource_underscore}.id }
375
+ }.to change(#{resource_name}, :count).by(-1)
376
+ end
377
+ end
378
+ end
379
+ RUBY
380
+
381
+ File.write(file_path, content)
382
+ puts " Created spec/requests/#{namespace}/#{version}/#{file_name}"
383
+ file_path
384
+ end
385
+
386
+ def self.find_rails_app_path
387
+ path = Dir.pwd
388
+ 10.times do
389
+ return path if File.exist?(File.join(path, "config", "application.rb"))
390
+ parent = File.dirname(path)
391
+ break if parent == path
392
+ path = parent
393
+ end
394
+ nil
395
+ end
396
+ end
397
+ end
@@ -0,0 +1,289 @@
1
+ # Audit module for RailsForge
2
+ # Runs a comprehensive architecture health check
3
+
4
+ module RailsForge
5
+ # Audit module combines all analyzers to produce a comprehensive report
6
+ module Audit
7
+ # Error class for audit issues
8
+ class AuditError < StandardError; end
9
+
10
+ # Default scoring weights for each analyzer
11
+ WEIGHTS = {
12
+ controllers: 25,
13
+ models: 25,
14
+ database: 20,
15
+ specs: 15,
16
+ metrics: 15
17
+ }.freeze
18
+
19
+ # Run full audit and return results
20
+ # @param base_path [String] Rails app root path
21
+ # @return [Hash] Complete audit results
22
+ def self.run(base_path = nil)
23
+ base_path ||= find_rails_app_path
24
+ raise AuditError, "Not in a Rails application directory" unless base_path
25
+
26
+ results = {
27
+ timestamp: Time.now.iso8601,
28
+ base_path: base_path,
29
+ controllers: run_controller_analysis(base_path),
30
+ models: run_model_analysis(base_path),
31
+ database: run_database_analysis(base_path),
32
+ specs: run_spec_analysis(base_path),
33
+ metrics: run_metrics_analysis(base_path)
34
+ }
35
+
36
+ results[:score] = calculate_score(results)
37
+ results
38
+ end
39
+
40
+ # Run controller analysis
41
+ # @param base_path [String] Rails app root path
42
+ # @return [Hash] Controller analysis results
43
+ def self.run_controller_analysis(base_path)
44
+ begin
45
+ issues = ControllerAnalyzer.analyze(base_path)
46
+ {
47
+ issues_count: issues.length,
48
+ issues: issues,
49
+ status: issues.empty? ? :pass : :warning
50
+ }
51
+ rescue => e
52
+ { issues_count: 0, issues: [], status: :error, error: e.message }
53
+ end
54
+ end
55
+
56
+ # Run model analysis
57
+ # @param base_path [String] Rails app root path
58
+ # @return [Hash] Model analysis results
59
+ def self.run_model_analysis(base_path)
60
+ begin
61
+ issues = ModelAnalyzer.analyze(base_path)
62
+ {
63
+ issues_count: issues.length,
64
+ issues: issues,
65
+ status: issues.empty? ? :pass : :warning
66
+ }
67
+ rescue => e
68
+ { issues_count: 0, issues: [], status: :error, error: e.message }
69
+ end
70
+ end
71
+
72
+ # Run database analysis
73
+ # @param base_path [String] Rails app root path
74
+ # @return [Hash] Database analysis results
75
+ def self.run_database_analysis(base_path)
76
+ begin
77
+ issues = DatabaseAnalyzer.analyze(base_path)
78
+ {
79
+ issues_count: issues.length,
80
+ issues: issues,
81
+ status: issues.empty? ? :pass : :warning
82
+ }
83
+ rescue => e
84
+ { issues_count: 0, issues: [], status: :error, error: e.message }
85
+ end
86
+ end
87
+
88
+ # Run spec analysis
89
+ # @param base_path [String] Rails app root path
90
+ # @return [Hash] Spec analysis results
91
+ def self.run_spec_analysis(base_path)
92
+ begin
93
+ results = SpecAnalyzer.analyze(base_path)
94
+ missing_count = results.count { |r| r[:has_spec] == false }
95
+ {
96
+ issues_count: missing_count,
97
+ coverage: results,
98
+ status: missing_count == 0 ? :pass : :warning
99
+ }
100
+ rescue => e
101
+ { issues_count: 0, coverage: [], status: :error, error: e.message }
102
+ end
103
+ end
104
+
105
+ # Run metrics analysis
106
+ # @param base_path [String] Rails app root path
107
+ # @return [Hash] Metrics analysis results
108
+ def self.run_metrics_analysis(base_path)
109
+ begin
110
+ metrics = MetricsAnalyzer.analyze(base_path)
111
+ {
112
+ metrics: metrics,
113
+ status: :pass
114
+ }
115
+ rescue => e
116
+ { metrics: {}, status: :error, error: e.message }
117
+ end
118
+ end
119
+
120
+ # Calculate overall architecture score
121
+ # @param results [Hash] All analysis results
122
+ # @return [Integer] Score out of 100
123
+ def self.calculate_score(results)
124
+ score = 100
125
+
126
+ # Deduct points for controller issues
127
+ controller_issues = results[:controllers][:issues_count]
128
+ score -= [controller_issues * 5, WEIGHTS[:controllers]].min
129
+
130
+ # Deduct points for model issues
131
+ model_issues = results[:models][:issues_count]
132
+ score -= [model_issues * 5, WEIGHTS[:models]].min
133
+
134
+ # Deduct points for database issues
135
+ db_issues = results[:database][:issues_count]
136
+ score -= [db_issues * 3, WEIGHTS[:database]].min
137
+
138
+ # Deduct points for missing specs
139
+ spec_issues = results[:specs][:issues_count]
140
+ score -= [spec_issues * 2, WEIGHTS[:specs]].min
141
+
142
+ [score, 0].max.to_i
143
+ end
144
+
145
+ # Format results as a readable report
146
+ # @param results [Hash] Audit results
147
+ # @return [String] Formatted report
148
+ def self.format_report(results)
149
+ score = results[:score]
150
+ score_color = score >= 80 ? :green : score >= 50 ? :yellow : :red
151
+
152
+ output = []
153
+ output << "=" * 60
154
+ output << "RailsForge Architecture Audit"
155
+ output << "=" * 60
156
+ output << ""
157
+ output << "Architecture Score: #{colorize(score, score_color)}/100"
158
+ output << ""
159
+
160
+ # Controllers section
161
+ output << "-" * 40
162
+ output << "Controllers:"
163
+ controller_issues = results[:controllers][:issues_count]
164
+ if controller_issues == 0
165
+ output << " ✓ No issues found (#{controller_issues})"
166
+ else
167
+ output << " ⚠ #{controller_issues} issue(s) found"
168
+ results[:controllers][:issues].first(5).each do |issue|
169
+ output << " - #{issue[:file]}: #{issue[:issues].join(', ')}"
170
+ end
171
+ output << " ..." if results[:controllers][:issues].count > 5
172
+ end
173
+ output << ""
174
+
175
+ # Models section
176
+ output << "-" * 40
177
+ output << "Models:"
178
+ model_issues = results[:models][:issues_count]
179
+ if model_issues == 0
180
+ output << " ✓ No issues found (#{model_issues})"
181
+ else
182
+ output << " ⚠ #{model_issues} issue(s) found"
183
+ results[:models][:issues].first(5).each do |issue|
184
+ output << " - #{issue[:file]}: #{issue[:issues].join(', ')}"
185
+ end
186
+ output << " ..." if results[:models][:issues].count > 5
187
+ end
188
+ output << ""
189
+
190
+ # Database section
191
+ output << "-" * 40
192
+ output << "Database:"
193
+ db_issues = results[:database][:issues_count]
194
+ if db_issues == 0
195
+ output << " ✓ No issues found (#{db_issues})"
196
+ else
197
+ output << " ⚠ #{db_issues} issue(s) found"
198
+ results[:database][:issues].first(5).each do |issue|
199
+ output << " - #{issue[:table]}: #{issue[:issue]}"
200
+ end
201
+ output << " ..." if results[:database][:issues].count > 5
202
+ end
203
+ output << ""
204
+
205
+ # Specs section
206
+ output << "-" * 40
207
+ output << "Specs:"
208
+ spec_issues = results[:specs][:issues_count]
209
+ if spec_issues == 0
210
+ output << " ✓ All components have specs (#{spec_issues} missing)"
211
+ else
212
+ output << " ⚠ #{spec_issues} component(s) missing specs"
213
+ results[:specs][:coverage].select { |r| r[:has_spec] == false }.first(5).each do |item|
214
+ output << " - #{item[:type]}: #{item[:name]}"
215
+ end
216
+ output << " ..." if results[:specs][:coverage].count { |r| r[:has_spec] == false } > 5
217
+ end
218
+ output << ""
219
+
220
+ # Metrics section
221
+ output << "-" * 40
222
+ output << "Metrics:"
223
+ metrics = results[:metrics][:metrics]
224
+ if metrics && !metrics.empty?
225
+ output << " Files: #{metrics[:total_files] || 'N/A'}"
226
+ output << " Lines: #{metrics[:total_lines] || 'N/A'}"
227
+ output << " Services: #{metrics[:services_count] || 0}"
228
+ output << " Queries: #{metrics[:queries_count] || 0}"
229
+ output << " Jobs: #{metrics[:jobs_count] || 0}"
230
+ else
231
+ output << " No metrics available"
232
+ end
233
+ output << ""
234
+
235
+ output << "=" * 60
236
+ output << "Audit completed at #{results[:timestamp]}"
237
+ output << "=" * 60
238
+
239
+ output.join("\n")
240
+ end
241
+
242
+ # Print results to console
243
+ # @param results [Hash] Audit results
244
+ def self.print_report(results)
245
+ puts format_report(results)
246
+ end
247
+
248
+ # Convert results to JSON
249
+ # @param results [Hash] Audit results
250
+ # @return [String] JSON string
251
+ def self.to_json(results)
252
+ require 'json'
253
+ # Remove non-serializable data
254
+ serializable = results.dup
255
+ serializable.delete(:timestamp)
256
+ JSON.pretty_generate(serializable)
257
+ end
258
+
259
+ # Find Rails app path
260
+ # @return [String, nil] Rails app root path
261
+ def self.find_rails_app_path
262
+ path = Dir.pwd
263
+ max_depth = 10
264
+
265
+ max_depth.times do
266
+ return path if File.exist?(File.join(path, "config", "application.rb"))
267
+ parent = File.dirname(path)
268
+ break if parent == path
269
+ path = parent
270
+ end
271
+
272
+ nil
273
+ end
274
+
275
+ # Colorize output for terminal
276
+ # @param text [String] Text to colorize
277
+ # @param color [Symbol] Color name
278
+ # @return [String] Colorized text
279
+ def self.colorize(text, color)
280
+ colors = {
281
+ green: "\e[32m",
282
+ yellow: "\e[33m",
283
+ red: "\e[31m",
284
+ reset: "\e[0m"
285
+ }
286
+ "#{colors[color]}#{text}#{colors[:reset]}"
287
+ end
288
+ end
289
+ end