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,181 @@
1
+ # Config module for RailsForge
2
+ # Manages .railsforgerc configuration
3
+
4
+ require 'yaml'
5
+ require 'fileutils'
6
+
7
+ module RailsForge
8
+ # Config class handles RailsForge configuration
9
+ class Config
10
+ # Default config file name
11
+ CONFIG_FILE = ".railsforgerc".freeze
12
+
13
+ # Default configuration
14
+ DEFAULT_CONFIG = {
15
+ "version" => "1.0",
16
+ "profile" => "standard",
17
+ "generators" => {
18
+ "default_template_version" => "v1",
19
+ "include_specs" => true
20
+ },
21
+ "analyzers" => {
22
+ "controller_max_lines" => 150,
23
+ "controller_max_methods" => 10,
24
+ "model_max_lines" => 200,
25
+ "model_max_method_lines" => 15
26
+ },
27
+ "refactor" => {
28
+ "min_method_lines" => 15,
29
+ "auto_extract" => false
30
+ }
31
+ }.freeze
32
+
33
+ # Load configuration from file
34
+ # @param base_path [String] Rails app root path
35
+ # @return [Hash] Configuration
36
+ def self.load(base_path = nil)
37
+ base_path ||= find_rails_app_path || Dir.pwd
38
+
39
+ config_file = File.join(base_path, CONFIG_FILE)
40
+
41
+ if File.exist?(config_file)
42
+ YAML.safe_load(File.read(config_file), permitted_classes: [], permitted_symbols: [], aliases: true) || DEFAULT_CONFIG
43
+ else
44
+ DEFAULT_CONFIG.dup
45
+ end
46
+ end
47
+
48
+ # Save configuration to file
49
+ # @param config [Hash] Configuration to save
50
+ # @param base_path [String] Rails app root path
51
+ def self.save(config, base_path = nil)
52
+ base_path ||= find_rails_app_path || Dir.pwd
53
+
54
+ config_file = File.join(base_path, CONFIG_FILE)
55
+
56
+ File.write(config_file, YAML.dump(config))
57
+ puts "Configuration saved to #{config_file}"
58
+ end
59
+
60
+ # Get a specific config value
61
+ # @param key [String] Configuration key (dot-notation supported)
62
+ # @param base_path [String] Rails app root path
63
+ # @return [Object] Configuration value
64
+ def self.get(key, base_path = nil)
65
+ config = load(base_path)
66
+
67
+ keys = key.split(".")
68
+ value = config
69
+
70
+ keys.each do |k|
71
+ value = value[k] if value.is_a?(Hash)
72
+ end
73
+
74
+ value
75
+ end
76
+
77
+ # Set a specific config value
78
+ # @param key [String] Configuration key
79
+ # @param value [Object] Value to set
80
+ # @param base_path [String] Rails app root path
81
+ def self.set(key, value, base_path = nil)
82
+ config = load(base_path)
83
+
84
+ keys = key.split(".")
85
+ current = config
86
+
87
+ keys[0...-1].each do |k|
88
+ current[k] ||= {}
89
+ current = current[k]
90
+ end
91
+
92
+ current[keys.last] = value
93
+ save(config, base_path)
94
+ end
95
+
96
+ # Initialize default config file
97
+ # @param base_path [String] Rails app root path
98
+ def self.init(base_path = nil)
99
+ base_path ||= find_rails_app_path || Dir.pwd
100
+
101
+ config_file = File.join(base_path, CONFIG_FILE)
102
+
103
+ if File.exist?(config_file)
104
+ puts "Config file already exists"
105
+ return false
106
+ end
107
+
108
+ save(DEFAULT_CONFIG.dup, base_path)
109
+ true
110
+ end
111
+
112
+ # Show current configuration
113
+ # @param base_path [String] Rails app root path
114
+ def self.show(base_path = nil)
115
+ config = load(base_path)
116
+
117
+ puts "RailsForge Configuration:"
118
+ puts ""
119
+
120
+ # Version
121
+ puts " Version: #{config['version']}"
122
+ puts " Profile: #{config['profile']}"
123
+ puts ""
124
+
125
+ # Generators
126
+ puts " Generators:"
127
+ gens = config["generators"] || {}
128
+ gens.each do |key, value|
129
+ puts " #{key}: #{value}"
130
+ end
131
+ puts ""
132
+
133
+ # Analyzers
134
+ puts " Analyzers:"
135
+ analyzers = config["analyzers"] || {}
136
+ analyzers.each do |key, value|
137
+ puts " #{key}: #{value}"
138
+ end
139
+ puts ""
140
+
141
+ # Refactor
142
+ puts " Refactor:"
143
+ refactor = config["refactor"] || {}
144
+ refactor.each do |key, value|
145
+ puts " #{key}: #{value}"
146
+ end
147
+ end
148
+
149
+ # Reset configuration to defaults
150
+ # @param base_path [String] Rails app root path
151
+ def self.reset(base_path = nil)
152
+ base_path ||= find_rails_app_path || Dir.pwd
153
+
154
+ config_file = File.join(base_path, CONFIG_FILE)
155
+
156
+ if File.exist?(config_file)
157
+ File.delete(config_file)
158
+ puts "Configuration reset to defaults"
159
+ else
160
+ puts "No configuration file to reset"
161
+ end
162
+ end
163
+
164
+ private
165
+
166
+ # Find Rails app path
167
+ def self.find_rails_app_path
168
+ path = Dir.pwd
169
+ max_depth = 10
170
+
171
+ max_depth.times do
172
+ return path if File.exist?(File.join(path, "config", "application.rb"))
173
+ parent = File.dirname(path)
174
+ break if parent == path
175
+ path = parent
176
+ end
177
+
178
+ nil
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,300 @@
1
+ module RailsForge
2
+ # DatabaseAnalyzer module scans models and database schema for issues
3
+ module DatabaseAnalyzer
4
+ # Error class for database analysis issues
5
+ class DatabaseError < 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 tableize helper
15
+ def self.tableize(word)
16
+ pluralize(word.downcase)
17
+ end
18
+
19
+ # Analyzes database and models for common issues
20
+ # @param base_path [String] Rails app root path
21
+ # @return [Array<Hash>] Analysis results
22
+ def self.analyze(base_path = nil)
23
+ base_path ||= find_rails_app_path
24
+ raise DatabaseError, "Not in a Rails application directory" unless base_path
25
+
26
+ results = []
27
+
28
+ # Analyze models
29
+ results += analyze_models(base_path)
30
+
31
+ # Analyze schema if available
32
+ results += analyze_schema(base_path)
33
+
34
+ results
35
+ end
36
+
37
+ # Analyzes models for missing indexes and constraints
38
+ # @param base_path [String] Rails app root path
39
+ # @return [Array<Hash>] Analysis results
40
+ def self.analyze_models(base_path)
41
+ results = []
42
+ models_dir = File.join(base_path, "app", "models")
43
+
44
+ return results unless Dir.exist?(models_dir)
45
+
46
+ Dir.glob(File.join(models_dir, "**", "*.rb")).each do |file|
47
+ next if file.end_with?("_application.rb")
48
+
49
+ model_name = File.basename(file, ".rb")
50
+ content = File.read(file)
51
+
52
+ # Skip abstract classes
53
+ next if content.include?("abstract_class = true")
54
+
55
+ # Check for missing indexes on foreign keys
56
+ results += check_foreign_keys(content, model_name)
57
+
58
+ # Check for uniqueness constraints
59
+ results += check_uniqueness(content, model_name)
60
+
61
+ # Check for missing database indexes
62
+ results += check_indexes(content, model_name)
63
+ end
64
+
65
+ results
66
+ end
67
+
68
+ # Analyzes schema.rb for missing foreign keys and indexes
69
+ # @param base_path [String] Rails app root path
70
+ # @return [Array<Hash>] Analysis results
71
+ def self.analyze_schema(base_path)
72
+ results = []
73
+ schema_file = File.join(base_path, "db", "schema.rb")
74
+
75
+ return results unless File.exist?(schema_file)
76
+
77
+ content = File.read(schema_file)
78
+
79
+ # Parse create_table statements
80
+ tables = content.scan(/create_table\s+"(\w+)"/).flatten
81
+
82
+ tables.each do |table|
83
+ # Check for tables without indexes
84
+ table_section = content[/create_table\s+"#{table}".*?(?=create_table|\z)/m]
85
+
86
+ if table_section
87
+ # Check for missing timestamps indexes
88
+ if table_section.include?("t.datetime")
89
+ results << {
90
+ type: :index,
91
+ table: table,
92
+ issue: "Table #{table} has datetime columns - consider adding indexes",
93
+ suggestion: "add_index :#{table}, :created_at",
94
+ severity: :low
95
+ } unless table_section.include?("index") && table.include?("created_at")
96
+ end
97
+ end
98
+ end
99
+
100
+ results
101
+ end
102
+
103
+ # Checks for missing foreign key indexes
104
+ # @param content [String] Model file content
105
+ # @param model_name [String] Model name
106
+ # @return [Array<Hash>] Analysis results
107
+ def self.check_foreign_keys(content, model_name)
108
+ results = []
109
+
110
+ # Find belongs_to associations
111
+ content.scan(/belongs_to\s+:(\w+)/).each do |assoc|
112
+ assoc_name = assoc[0]
113
+
114
+ # Check if foreign key is indexed
115
+ unless content.include?("index") && content.include?("#{assoc_name}_id")
116
+ results << {
117
+ type: :foreign_key,
118
+ model: model_name,
119
+ assoc: assoc_name,
120
+ issue: "Model #{model_name} has belongs_to :#{assoc_name} but may be missing an index",
121
+ suggestion: "add_index :#{tableize(model_name)}, :#{assoc_name}_id",
122
+ severity: :medium
123
+ }
124
+ end
125
+ end
126
+
127
+ results
128
+ end
129
+
130
+ # Checks for uniqueness validations without database constraints
131
+ # @param content [String] Model file content
132
+ # @param model_name [String] Model name
133
+ # @return [Array<Hash>] Analysis results
134
+ def self.check_uniqueness(content, model_name)
135
+ results = []
136
+
137
+ # Find uniqueness validations
138
+ content.scan(/validates\s+:(\w+),\s+uniqueness:/).each do |field|
139
+ field_name = field[0]
140
+
141
+ # Check if there's a unique index
142
+ unless content.include?("unique: true")
143
+ results << {
144
+ type: :uniqueness,
145
+ model: model_name,
146
+ field: field_name,
147
+ issue: "Model #{model_name} validates uniqueness of :#{field_name} but has no unique index",
148
+ suggestion: "add_index :#{tableize(model_name)}, :#{field_name}, unique: true",
149
+ severity: :high
150
+ }
151
+ end
152
+ end
153
+
154
+ results
155
+ end
156
+
157
+ # Checks for commonly indexed fields that may be missing indexes
158
+ # @param content [String] Model file content
159
+ # @param model_name [String] Model name
160
+ # @return [Array<Hash>] Analysis results
161
+ def self.check_indexes(content, model_name)
162
+ results = []
163
+
164
+ # Common fields that should be indexed
165
+ commonly_indexed = %w[email slug token uuid]
166
+
167
+ commonly_indexed.each do |field|
168
+ if content.include?(":#{field}") && !content.include?("add_index")
169
+ results << {
170
+ type: :index,
171
+ model: model_name,
172
+ issue: "Model #{model_name} has :#{field} field but may be missing an index",
173
+ suggestion: "add_index :#{tableize(model_name)}, :#{field}",
174
+ severity: :medium
175
+ }
176
+ end
177
+ end
178
+
179
+ # Check for has_many :through associations that need indexes
180
+ content.scan(/has_many\s+:(\w+),\s+through:/).each do |assoc|
181
+ results << {
182
+ type: :index,
183
+ model: model_name,
184
+ issue: "Model #{model_name} has has_many :through :#{assoc[0]} - ensure join table has indexes",
185
+ suggestion: "Ensure your join table has composite indexes on both foreign keys",
186
+ severity: :low
187
+ }
188
+ end
189
+
190
+ results
191
+ end
192
+
193
+ # Generates a migration file for a suggested fix
194
+ # @param suggestion [Hash] Suggestion details
195
+ # @param base_path [String] Rails app root
196
+ # @return [String] Path to created migration
197
+ def self.generate_migration(suggestion, base_path = nil)
198
+ base_path ||= find_rails_app_path
199
+ raise DatabaseError, "Not in a Rails application directory" unless base_path
200
+
201
+ migrate_dir = File.join(base_path, "db", "migrate")
202
+ FileUtils.mkdir_p(migrate_dir)
203
+
204
+ timestamp = Time.now.strftime("%Y%m%d%H%M%S")
205
+ filename = "migration_#{timestamp}.rb"
206
+ filepath = File.join(migrate_dir, filename)
207
+
208
+ table_name = tableize(suggestion[:model])
209
+
210
+ migration_content = case suggestion[:type]
211
+ when :uniqueness
212
+ field = suggestion[:field] || "field"
213
+ <<~RUBY
214
+ class Add#{suggestion[:model]}#{field.capitalize}UniqueIndex < ActiveRecord::Migration[7.0]
215
+ def change
216
+ add_index :#{table_name}, :#{field}, unique: true
217
+ end
218
+ end
219
+ RUBY
220
+ when :foreign_key
221
+ assoc = suggestion[:assoc] || "association"
222
+ <<~RUBY
223
+ class Add#{suggestion[:model].capitalize}#{assoc.capitalize}Index < ActiveRecord::Migration[7.0]
224
+ def change
225
+ add_index :#{table_name}, :#{assoc}_id
226
+ end
227
+ end
228
+ RUBY
229
+ when :index
230
+ <<~RUBY
231
+ class Add#{suggestion[:model].capitalize}Index < ActiveRecord::Migration[7.0]
232
+ def change
233
+ #{suggestion[:suggestion]}
234
+ end
235
+ end
236
+ RUBY
237
+ else
238
+ "# No migration template available for #{suggestion[:type]}"
239
+ end
240
+
241
+ File.write(filepath, migration_content)
242
+ puts " Created db/migrate/#{filename}"
243
+ filepath
244
+ end
245
+
246
+ # Prints analysis report
247
+ # @param results [Array<Hash>] Analysis results
248
+ def self.print_report(results)
249
+ return puts "\n✓ No database issues found!" if results.empty?
250
+
251
+ puts "\n" + "=" * 60
252
+ puts "DATABASE ANALYSIS REPORT"
253
+ puts "=" * 60
254
+
255
+ # Group by severity
256
+ high = results.select { |r| r[:severity] == :high }
257
+ medium = results.select { |r| r[:severity] == :medium }
258
+ low = results.select { |r| r[:severity] == :low }
259
+
260
+ if high.any?
261
+ puts "\n🔴 High Priority (#{high.count}):"
262
+ high.each { |r| print_issue(r) }
263
+ end
264
+
265
+ if medium.any?
266
+ puts "\n🟡 Medium Priority (#{medium.count}):"
267
+ medium.each { |r| print_issue(r) }
268
+ end
269
+
270
+ if low.any?
271
+ puts "\n🟢 Low Priority (#{low.count}):"
272
+ low.each { |r| print_issue(r) }
273
+ end
274
+
275
+ puts "\n" + "=" * 60
276
+ puts "Total issues found: #{results.count}"
277
+ puts "=" * 60
278
+ end
279
+
280
+ # Prints a single issue
281
+ # @param issue [Hash] Issue details
282
+ def self.print_issue(issue)
283
+ puts "\n #{issue[:type].to_s.upcase}: #{issue[:issue]}"
284
+ puts " → Suggested: #{issue[:suggestion]}"
285
+ end
286
+
287
+ # Finds Rails app root path
288
+ # @return [String, nil] Rails app path
289
+ def self.find_rails_app_path
290
+ path = Dir.pwd
291
+ 10.times do
292
+ return path if File.exist?(File.join(path, "config", "application.rb"))
293
+ parent = File.dirname(path)
294
+ break if parent == path
295
+ path = parent
296
+ end
297
+ nil
298
+ end
299
+ end
300
+ end