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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +528 -0
- data/bin/railsforge +8 -0
- data/lib/railsforge/analyzers/base_analyzer.rb +41 -0
- data/lib/railsforge/analyzers/controller_analyzer.rb +83 -0
- data/lib/railsforge/analyzers/database_analyzer.rb +55 -0
- data/lib/railsforge/analyzers/metrics_analyzer.rb +55 -0
- data/lib/railsforge/analyzers/model_analyzer.rb +74 -0
- data/lib/railsforge/analyzers/performance_analyzer.rb +161 -0
- data/lib/railsforge/analyzers/refactor_analyzer.rb +118 -0
- data/lib/railsforge/analyzers/security_analyzer.rb +169 -0
- data/lib/railsforge/analyzers/spec_analyzer.rb +58 -0
- data/lib/railsforge/api_generator.rb +397 -0
- data/lib/railsforge/audit.rb +289 -0
- data/lib/railsforge/cli.rb +671 -0
- data/lib/railsforge/config.rb +181 -0
- data/lib/railsforge/database_analyzer.rb +300 -0
- data/lib/railsforge/doctor.rb +250 -0
- data/lib/railsforge/feature_generator.rb +560 -0
- data/lib/railsforge/generator.rb +313 -0
- data/lib/railsforge/generators/base_generator.rb +70 -0
- data/lib/railsforge/generators/demo_generator.rb +307 -0
- data/lib/railsforge/generators/devops_generator.rb +287 -0
- data/lib/railsforge/generators/monitoring_generator.rb +134 -0
- data/lib/railsforge/generators/service_generator.rb +122 -0
- data/lib/railsforge/generators/stimulus_controller_generator.rb +129 -0
- data/lib/railsforge/generators/test_generator.rb +289 -0
- data/lib/railsforge/generators/view_component_generator.rb +169 -0
- data/lib/railsforge/graph.rb +270 -0
- data/lib/railsforge/loader.rb +56 -0
- data/lib/railsforge/mailer_generator.rb +191 -0
- data/lib/railsforge/plugins/plugin_loader.rb +60 -0
- data/lib/railsforge/plugins.rb +30 -0
- data/lib/railsforge/profiles/admin_app.yml +49 -0
- data/lib/railsforge/profiles/api_only.yml +47 -0
- data/lib/railsforge/profiles/blog.yml +47 -0
- data/lib/railsforge/profiles/standard.yml +44 -0
- data/lib/railsforge/profiles.rb +99 -0
- data/lib/railsforge/refactor_analyzer.rb +401 -0
- data/lib/railsforge/refactor_controller.rb +277 -0
- data/lib/railsforge/refactors/refactor_controller.rb +117 -0
- data/lib/railsforge/template_loader.rb +105 -0
- data/lib/railsforge/templates/v1/form/spec_template.rb +18 -0
- data/lib/railsforge/templates/v1/form/template.rb +28 -0
- data/lib/railsforge/templates/v1/job/spec_template.rb +17 -0
- data/lib/railsforge/templates/v1/job/template.rb +13 -0
- data/lib/railsforge/templates/v1/policy/spec_template.rb +41 -0
- data/lib/railsforge/templates/v1/policy/template.rb +57 -0
- data/lib/railsforge/templates/v1/presenter/spec_template.rb +12 -0
- data/lib/railsforge/templates/v1/presenter/template.rb +13 -0
- data/lib/railsforge/templates/v1/query/spec_template.rb +12 -0
- data/lib/railsforge/templates/v1/query/template.rb +16 -0
- data/lib/railsforge/templates/v1/serializer/spec_template.rb +13 -0
- data/lib/railsforge/templates/v1/serializer/template.rb +11 -0
- data/lib/railsforge/templates/v1/service/spec_template.rb +12 -0
- data/lib/railsforge/templates/v1/service/template.rb +25 -0
- data/lib/railsforge/templates/v1/stimulus_controller/template.rb +35 -0
- data/lib/railsforge/templates/v1/view_component/template.rb +24 -0
- data/lib/railsforge/templates/v2/job/template.rb +49 -0
- data/lib/railsforge/templates/v2/query/template.rb +66 -0
- data/lib/railsforge/templates/v2/service/spec_template.rb +33 -0
- data/lib/railsforge/templates/v2/service/template.rb +71 -0
- data/lib/railsforge/templates/v3/job/template.rb +72 -0
- data/lib/railsforge/templates/v3/query/spec_template.rb +54 -0
- data/lib/railsforge/templates/v3/query/template.rb +115 -0
- data/lib/railsforge/templates/v3/service/spec_template.rb +51 -0
- data/lib/railsforge/templates/v3/service/template.rb +84 -0
- data/lib/railsforge/version.rb +5 -0
- data/lib/railsforge/wizard.rb +265 -0
- data/lib/railsforge/wizard_tui.rb +286 -0
- data/lib/railsforge.rb +13 -0
- metadata +216 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Service class template v3
|
|
2
|
+
# Advanced service with result monad and chaining
|
|
3
|
+
class <%= class_name %>
|
|
4
|
+
include Dry::Monads[:result, :do]
|
|
5
|
+
|
|
6
|
+
# Initialize with dependencies
|
|
7
|
+
# @param [Hash] params - Parameters for the service
|
|
8
|
+
def initialize(params = {})
|
|
9
|
+
@params = params
|
|
10
|
+
@dependencies = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Register a dependency
|
|
14
|
+
# @param [Symbol] key - Dependency key
|
|
15
|
+
# @param [Object] value - Dependency value
|
|
16
|
+
def register(key, value)
|
|
17
|
+
@dependencies[key] = value
|
|
18
|
+
self
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Execute the service - returns a Dry::Monads Result
|
|
22
|
+
def call
|
|
23
|
+
yield validate!
|
|
24
|
+
yield process!
|
|
25
|
+
Success(result_data)
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
Failure(error: e.message, code: :unexpected_error)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Chain another service
|
|
31
|
+
# @param [Class] service_class - Next service to call
|
|
32
|
+
# @param [Hash] params - Parameters for next service
|
|
33
|
+
def then(service_class, params = {})
|
|
34
|
+
result = call
|
|
35
|
+
return result unless result.success?
|
|
36
|
+
|
|
37
|
+
service = service_class.new(@params.merge(params)).call
|
|
38
|
+
service
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Class-level call
|
|
42
|
+
def self.call(params = {})
|
|
43
|
+
new(params).call
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# Validate inputs - returns Result
|
|
49
|
+
def validate!
|
|
50
|
+
# TODO: Add validation logic
|
|
51
|
+
# Example:
|
|
52
|
+
# return Failure(errors: ['Name required']) if @params[:name].blank?
|
|
53
|
+
Success(true)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Process the main logic - returns Result
|
|
57
|
+
def process!
|
|
58
|
+
# TODO: Implement service logic
|
|
59
|
+
@result = {}
|
|
60
|
+
Success(true)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Build result data
|
|
64
|
+
def result_data
|
|
65
|
+
{
|
|
66
|
+
data: @result,
|
|
67
|
+
metadata: {
|
|
68
|
+
service: self.class.name,
|
|
69
|
+
timestamp: Time.now.utc.iso8601
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Result wrapper for success/failure
|
|
76
|
+
module Result
|
|
77
|
+
def self.success(data)
|
|
78
|
+
{ success: true, data: data }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.failure(errors)
|
|
82
|
+
{ success: false, errors: errors }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# Wizard module for RailsForge
|
|
2
|
+
# Interactive project scaffolding
|
|
3
|
+
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module RailsForge
|
|
7
|
+
# Wizard class provides interactive project setup
|
|
8
|
+
class Wizard
|
|
9
|
+
# Project types available
|
|
10
|
+
PROJECT_TYPES = {
|
|
11
|
+
"saas" => {
|
|
12
|
+
name: "SaaS Application",
|
|
13
|
+
description: "Full-featured SaaS with auth, admin, payments",
|
|
14
|
+
features: %w[authentication admin background_jobs]
|
|
15
|
+
},
|
|
16
|
+
"blog" => {
|
|
17
|
+
name: "Blog",
|
|
18
|
+
description: "Content blog with articles and comments",
|
|
19
|
+
features: %w[authentication comments]
|
|
20
|
+
},
|
|
21
|
+
"api" => {
|
|
22
|
+
name: "API Only",
|
|
23
|
+
description: "RESTful API with JWT authentication",
|
|
24
|
+
features: %w[jwt_auth background_jobs]
|
|
25
|
+
},
|
|
26
|
+
"admin_app" => {
|
|
27
|
+
name: "Admin Dashboard",
|
|
28
|
+
description: "Admin panel with CRUD operations",
|
|
29
|
+
features: %w[authentication admin]
|
|
30
|
+
},
|
|
31
|
+
"standard" => {
|
|
32
|
+
name: "Standard Rails",
|
|
33
|
+
description: "Basic Rails application",
|
|
34
|
+
features: []
|
|
35
|
+
}
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
# Initialize wizard
|
|
39
|
+
def initialize
|
|
40
|
+
@options = {
|
|
41
|
+
project_name: nil,
|
|
42
|
+
project_type: nil,
|
|
43
|
+
features: [],
|
|
44
|
+
generators: [],
|
|
45
|
+
analyze_after: false
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Run the interactive wizard
|
|
50
|
+
# @return [Hash] Selected options
|
|
51
|
+
def run
|
|
52
|
+
puts ""
|
|
53
|
+
puts "=" * 60
|
|
54
|
+
puts "RailsForge Project Wizard"
|
|
55
|
+
puts "=" * 60
|
|
56
|
+
puts ""
|
|
57
|
+
|
|
58
|
+
# Step 1: Project name
|
|
59
|
+
ask_project_name
|
|
60
|
+
|
|
61
|
+
# Step 2: Project type
|
|
62
|
+
ask_project_type
|
|
63
|
+
|
|
64
|
+
# Step 3: Features
|
|
65
|
+
ask_features
|
|
66
|
+
|
|
67
|
+
# Step 4: Generators
|
|
68
|
+
ask_generators
|
|
69
|
+
|
|
70
|
+
# Step 5: Analysis
|
|
71
|
+
ask_analysis
|
|
72
|
+
|
|
73
|
+
# Summary
|
|
74
|
+
show_summary
|
|
75
|
+
|
|
76
|
+
@options
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Execute the wizard choices
|
|
80
|
+
def execute
|
|
81
|
+
return unless @options[:project_name]
|
|
82
|
+
|
|
83
|
+
puts ""
|
|
84
|
+
puts "Creating Rails app: #{@options[:project_name]}"
|
|
85
|
+
puts ""
|
|
86
|
+
|
|
87
|
+
# Build options for RailsForge::Generator
|
|
88
|
+
generator_options = {
|
|
89
|
+
profile: @options[:project_type]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# Add features
|
|
93
|
+
generator_options[:auth] = "devise" if @options[:features].include?("authentication")
|
|
94
|
+
generator_options[:admin] = "activeadmin" if @options[:features].include?("admin")
|
|
95
|
+
generator_options[:jobs] = "sidekiq" if @options[:features].include?("background_jobs")
|
|
96
|
+
|
|
97
|
+
begin
|
|
98
|
+
# Create the Rails app
|
|
99
|
+
result = RailsForge::Generator.create_app(@options[:project_name], generator_options)
|
|
100
|
+
puts result
|
|
101
|
+
puts ""
|
|
102
|
+
|
|
103
|
+
# Run selected generators
|
|
104
|
+
run_generators if @options[:generators].any?
|
|
105
|
+
|
|
106
|
+
# Run analysis if requested
|
|
107
|
+
run_analysis if @options[:analyze_after]
|
|
108
|
+
|
|
109
|
+
puts ""
|
|
110
|
+
puts "✓ Project created successfully!"
|
|
111
|
+
rescue => e
|
|
112
|
+
puts "Error: #{e.message}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
# Ask for project name
|
|
119
|
+
def ask_project_name
|
|
120
|
+
print "Enter project name: "
|
|
121
|
+
name = STDIN.gets.strip
|
|
122
|
+
|
|
123
|
+
if name.empty?
|
|
124
|
+
puts "Please enter a valid project name"
|
|
125
|
+
ask_project_name
|
|
126
|
+
elsif name =~ /\A\d/ || name =~ /\s/
|
|
127
|
+
puts "Project name cannot start with a number or contain spaces"
|
|
128
|
+
ask_project_name
|
|
129
|
+
else
|
|
130
|
+
@options[:project_name] = name
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Ask for project type
|
|
135
|
+
def ask_project_type
|
|
136
|
+
puts ""
|
|
137
|
+
puts "Select project type:"
|
|
138
|
+
puts ""
|
|
139
|
+
|
|
140
|
+
PROJECT_TYPES.each do |key, type|
|
|
141
|
+
puts " #{key.ljust(12)} - #{type[:name]}"
|
|
142
|
+
puts " #{type[:description]}"
|
|
143
|
+
puts ""
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
print "Enter type (default: standard): "
|
|
147
|
+
type = STDIN.gets.strip.downcase
|
|
148
|
+
|
|
149
|
+
if type.empty?
|
|
150
|
+
@options[:project_type] = "standard"
|
|
151
|
+
elsif PROJECT_TYPES.key?(type)
|
|
152
|
+
@options[:project_type] = type
|
|
153
|
+
@options[:features] = PROJECT_TYPES[type][:features].dup
|
|
154
|
+
else
|
|
155
|
+
puts "Invalid project type"
|
|
156
|
+
ask_project_type
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Ask for additional features
|
|
161
|
+
def ask_features
|
|
162
|
+
puts ""
|
|
163
|
+
puts "Select additional features (comma-separated, or 'done'):"
|
|
164
|
+
puts ""
|
|
165
|
+
|
|
166
|
+
available_features = [
|
|
167
|
+
["authentication", "User authentication (Devise)"],
|
|
168
|
+
["admin", "Admin panel (ActiveAdmin)"],
|
|
169
|
+
["background_jobs", "Background jobs (Sidekiq)"],
|
|
170
|
+
["api", "REST API endpoints"],
|
|
171
|
+
["comments", "Comment system"],
|
|
172
|
+
["notifications", "Email/push notifications"],
|
|
173
|
+
["payments", "Payment integration (Stripe)"],
|
|
174
|
+
["uploads", "File uploads (Carrierwave/ActiveStorage)"]
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
available_features.each do |key, desc|
|
|
178
|
+
included = @options[:features].include?(key) ? " ✓" : ""
|
|
179
|
+
puts " #{key.ljust(20)} - #{desc}#{included}"
|
|
180
|
+
end
|
|
181
|
+
puts ""
|
|
182
|
+
|
|
183
|
+
print "Features (or 'done' to continue): "
|
|
184
|
+
features_input = STDIN.gets.strip.downcase
|
|
185
|
+
|
|
186
|
+
if features_input != "done"
|
|
187
|
+
selected = features_input.split(",").map(&:strip).reject(&:empty?)
|
|
188
|
+
@options[:features] = (@options[:features] + selected).uniq
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Ask which generators to run
|
|
193
|
+
def ask_generators
|
|
194
|
+
puts ""
|
|
195
|
+
puts "Select generators to run after creation (comma-separated, or 'none'):"
|
|
196
|
+
puts ""
|
|
197
|
+
|
|
198
|
+
available_generators = [
|
|
199
|
+
["scaffold", "Full scaffold (model, controller, views)"],
|
|
200
|
+
["resource", "API resource only"],
|
|
201
|
+
["service", "Example service object"],
|
|
202
|
+
["job", "Example background job"]
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
available_generators.each do |key, desc|
|
|
206
|
+
puts " #{key.ljust(12)} - #{desc}"
|
|
207
|
+
end
|
|
208
|
+
puts ""
|
|
209
|
+
|
|
210
|
+
print "Generators (or 'none'): "
|
|
211
|
+
generators_input = STDIN.gets.strip.downcase
|
|
212
|
+
|
|
213
|
+
if generators_input != "none" && !generators_input.empty?
|
|
214
|
+
@options[:generators] = generators_input.split(",").map(&:strip).reject(&:empty?)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Ask whether to run analysis
|
|
219
|
+
def ask_analysis
|
|
220
|
+
puts ""
|
|
221
|
+
print "Run architecture analysis after creation? (y/N): "
|
|
222
|
+
answer = STDIN.gets.strip.downcase
|
|
223
|
+
|
|
224
|
+
@options[:analyze_after] = answer == "y" || answer == "yes"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Show summary of choices
|
|
228
|
+
def show_summary
|
|
229
|
+
puts ""
|
|
230
|
+
puts "-" * 40
|
|
231
|
+
puts "Summary:"
|
|
232
|
+
puts " Project: #{@options[:project_name]}"
|
|
233
|
+
puts " Type: #{@options[:project_type]} (#{PROJECT_TYPES[@options[:project_type]][:name]})"
|
|
234
|
+
puts " Features: #{@options[:features].join(', ')}"
|
|
235
|
+
puts " Generators: #{@options[:generators].join(', ')}"
|
|
236
|
+
puts " Analyze: #{@options[:analyze_after] ? 'Yes' : 'No'}"
|
|
237
|
+
puts "-" * 40
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Run selected generators
|
|
241
|
+
def run_generators
|
|
242
|
+
puts ""
|
|
243
|
+
puts "Running generators..."
|
|
244
|
+
|
|
245
|
+
Dir.chdir(@options[:project_name]) do
|
|
246
|
+
@options[:generators].each do |generator|
|
|
247
|
+
puts " Running #{generator} generator..."
|
|
248
|
+
# In a full implementation, this would run the actual generators
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Run analysis
|
|
254
|
+
def run_analysis
|
|
255
|
+
puts ""
|
|
256
|
+
puts "Running architecture analysis..."
|
|
257
|
+
|
|
258
|
+
Dir.chdir(@options[:project_name]) do
|
|
259
|
+
# Run the doctor
|
|
260
|
+
doctor = RailsForge::Doctor.new
|
|
261
|
+
doctor.run
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# WizardTUI - Menu-driven interactive terminal interface for RailsForge
|
|
2
|
+
# Uses TTY::Prompt for interactive menus
|
|
3
|
+
|
|
4
|
+
module RailsForge
|
|
5
|
+
# WizardTUI - Interactive TUI wizard
|
|
6
|
+
class WizardTUI
|
|
7
|
+
# Available project types mapped to profiles
|
|
8
|
+
PROJECT_TYPES = {
|
|
9
|
+
"standard" => { name: "Standard", description: "Basic Rails application", profile: "standard" },
|
|
10
|
+
"blog" => { name: "Blog", description: "Content blog with articles and comments", profile: "blog" },
|
|
11
|
+
"admin_app" => { name: "Admin App", description: "Admin dashboard with CRUD operations", profile: "admin_app" },
|
|
12
|
+
"api_only" => { name: "API-Only", description: "RESTful API with JWT authentication", profile: "api_only" },
|
|
13
|
+
"saas" => { name: "SaaS", description: "Full-featured SaaS with auth, admin, payments", profile: "standard" }
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
# Available features/generators
|
|
17
|
+
AVAILABLE_FEATURES = %w[
|
|
18
|
+
services queries jobs forms presenters policies serializers
|
|
19
|
+
mailers features apis components stimulus
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
22
|
+
# Available analyzers
|
|
23
|
+
AVAILABLE_ANALYZERS = %w[
|
|
24
|
+
controllers models specs database metrics
|
|
25
|
+
security performance
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
# Initialize wizard
|
|
29
|
+
def initialize
|
|
30
|
+
# Lazy-load TTY dependencies
|
|
31
|
+
require 'tty-prompt'
|
|
32
|
+
require 'tty-table'
|
|
33
|
+
|
|
34
|
+
@prompt = TTY::Prompt.new
|
|
35
|
+
@options = {
|
|
36
|
+
project_name: nil,
|
|
37
|
+
project_type: nil,
|
|
38
|
+
template_version: nil,
|
|
39
|
+
features: [],
|
|
40
|
+
analyzers: [],
|
|
41
|
+
dry_run: false
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Main execution method
|
|
46
|
+
def run
|
|
47
|
+
puts ""
|
|
48
|
+
puts "🚀 Welcome to RailsForge Wizard!".color(:green)
|
|
49
|
+
puts "=" * 50
|
|
50
|
+
puts ""
|
|
51
|
+
|
|
52
|
+
# Step 1: Project name
|
|
53
|
+
ask_project_name
|
|
54
|
+
|
|
55
|
+
# Step 2: Project type
|
|
56
|
+
select_project_type
|
|
57
|
+
|
|
58
|
+
# Step 3: Template version
|
|
59
|
+
select_template_version
|
|
60
|
+
|
|
61
|
+
# Step 4: Features
|
|
62
|
+
select_features
|
|
63
|
+
|
|
64
|
+
# Step 5: Analyzers
|
|
65
|
+
select_analyzers
|
|
66
|
+
|
|
67
|
+
# Step 6: Dry-run option
|
|
68
|
+
ask_dry_run
|
|
69
|
+
|
|
70
|
+
# Step 7: Summary and confirmation
|
|
71
|
+
confirm_and_run
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Ask for project name
|
|
77
|
+
def ask_project_name
|
|
78
|
+
@options[:project_name] = @prompt.ask("Project name:") do |q|
|
|
79
|
+
q.required true
|
|
80
|
+
q.validate(/^[a-z][a-z0-9_]*$/i, "Must be valid Ruby identifier (letters, numbers, underscores)")
|
|
81
|
+
q.modify :strip
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Select project type
|
|
86
|
+
def select_project_type
|
|
87
|
+
choices = PROJECT_TYPES.map do |key, value|
|
|
88
|
+
{ name: "#{value[:name]} - #{value[:description]}", value: key }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
@options[:project_type] = @prompt.select("Select project type:", choices, default: "standard")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Select template version
|
|
95
|
+
def select_template_version
|
|
96
|
+
versions = detect_template_versions
|
|
97
|
+
|
|
98
|
+
# Check for default in config
|
|
99
|
+
default_version = RailsForge::Config.get("generators.default_template_version") rescue "v1"
|
|
100
|
+
|
|
101
|
+
choices = versions.map { |v| { name: "Version #{v}", value: v } }
|
|
102
|
+
@options[:template_version] = @prompt.select("Select template version:", choices, default: default_version)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Detect available template versions
|
|
106
|
+
def detect_template_versions
|
|
107
|
+
templates_dir = File.expand_path("../templates", __FILE__)
|
|
108
|
+
return ["v1"] unless Dir.exist?(templates_dir)
|
|
109
|
+
|
|
110
|
+
Dir.glob(File.join(templates_dir, "v*")).map do |dir|
|
|
111
|
+
File.basename(dir)
|
|
112
|
+
end.sort
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Select features
|
|
116
|
+
def select_features
|
|
117
|
+
feature_descriptions = {
|
|
118
|
+
"services" => "Service objects for business logic",
|
|
119
|
+
"queries" => "Query objects for database queries",
|
|
120
|
+
"jobs" => "Background jobs",
|
|
121
|
+
"forms" => "Form objects",
|
|
122
|
+
"presenters" => "Presenter objects",
|
|
123
|
+
"policies" => "Policy objects",
|
|
124
|
+
"serializers" => "Serializer objects",
|
|
125
|
+
"mailers" => "Mailer classes",
|
|
126
|
+
"features" => "Feature specs",
|
|
127
|
+
"apis" => "API resources",
|
|
128
|
+
"components" => "ViewComponent components",
|
|
129
|
+
"stimulus" => "Stimulus controllers"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
choices = AVAILABLE_FEATURES.map do |f|
|
|
133
|
+
{ name: "#{f.capitalize.ljust(15)} - #{feature_descriptions[f]}", value: f }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
@options[:features] = @prompt.multi_select("Select features to include:", choices, min: 0)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Select analyzers
|
|
140
|
+
def select_analyzers
|
|
141
|
+
analyzer_descriptions = {
|
|
142
|
+
"controllers" => "Analyze controllers for issues",
|
|
143
|
+
"models" => "Analyze models for issues",
|
|
144
|
+
"specs" => "Check spec coverage",
|
|
145
|
+
"database" => "Check database schema",
|
|
146
|
+
"metrics" => "Code metrics analysis",
|
|
147
|
+
"security" => "Security vulnerability scan",
|
|
148
|
+
"performance" => "Performance issue detection"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
choices = AVAILABLE_ANALYZERS.map do |a|
|
|
152
|
+
{ name: "#{a.capitalize.ljust(15)} - #{analyzer_descriptions[a]}", value: a }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Add "Run All" option
|
|
156
|
+
all_choice = { name: "Run All Analyzers", value: "all" }
|
|
157
|
+
choices.unshift(all_choice)
|
|
158
|
+
|
|
159
|
+
selected = @prompt.multi_select("Select analyzers to run after generation:", choices, min: 0)
|
|
160
|
+
|
|
161
|
+
# Handle "all" selection
|
|
162
|
+
@options[:analyzers] = selected.include?("all") ? AVAILABLE_ANALYZERS.dup : selected
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Ask for dry-run
|
|
166
|
+
def ask_dry_run
|
|
167
|
+
@options[:dry_run] = @prompt.yes?("Preview files without creating?")
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Display summary and confirm
|
|
171
|
+
def confirm_and_run
|
|
172
|
+
puts ""
|
|
173
|
+
puts "📋 Summary".color(:cyan)
|
|
174
|
+
puts "-" * 40
|
|
175
|
+
puts "Project: #{@options[:project_name]}"
|
|
176
|
+
puts "Type: #{PROJECT_TYPES[@options[:project_type]][:name]}"
|
|
177
|
+
puts "Template: #{@options[:template_version]}"
|
|
178
|
+
puts "Features: #{@options[:features].join(', ')}"
|
|
179
|
+
puts "Analyzers: #{@options[:analyzers].join(', ')}"
|
|
180
|
+
puts "Dry-run: #{@options[:dry_run] ? 'Yes' : 'No'}"
|
|
181
|
+
puts "-" * 40
|
|
182
|
+
|
|
183
|
+
confirmed = @prompt.yes?("Proceed with generation?")
|
|
184
|
+
|
|
185
|
+
if confirmed
|
|
186
|
+
execute
|
|
187
|
+
else
|
|
188
|
+
puts "Cancelled.".color(:yellow)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Execute the wizard choices
|
|
193
|
+
def execute
|
|
194
|
+
puts ""
|
|
195
|
+
puts "� Generating Rails app...".color(:green)
|
|
196
|
+
|
|
197
|
+
if @options[:dry_run]
|
|
198
|
+
dry_run_output
|
|
199
|
+
else
|
|
200
|
+
run_generation
|
|
201
|
+
run_analyzers
|
|
202
|
+
print_results
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Dry-run output
|
|
207
|
+
def dry_run_output
|
|
208
|
+
puts ""
|
|
209
|
+
puts "🔍 Dry-run mode - Preview of files to generate".color(:yellow)
|
|
210
|
+
puts ""
|
|
211
|
+
|
|
212
|
+
# Show what would be created
|
|
213
|
+
puts "Profile: #{@options[:project_type]}"
|
|
214
|
+
puts "Template version: #{@options[:template_version]}"
|
|
215
|
+
puts ""
|
|
216
|
+
puts "Features to generate:"
|
|
217
|
+
@options[:features].each { |f| puts " - #{f}" }
|
|
218
|
+
puts ""
|
|
219
|
+
puts "Analyzers to run:"
|
|
220
|
+
@options[:analyzers].each { |a| puts " - #{a}" }
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Run the actual generation
|
|
224
|
+
def run_generation
|
|
225
|
+
# Apply profile
|
|
226
|
+
profile = RailsForge::Profile.load(@options[:project_type])
|
|
227
|
+
RailsForge::Profile.create_folders(@options[:project_name], profile)
|
|
228
|
+
|
|
229
|
+
# Run selected generators
|
|
230
|
+
@options[:features].each do |feature|
|
|
231
|
+
puts " Running #{feature} generator..."
|
|
232
|
+
# Note: In a full implementation, this would run the actual generators
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Run selected analyzers
|
|
237
|
+
def run_analyzers
|
|
238
|
+
return if @options[:analyzers].empty?
|
|
239
|
+
|
|
240
|
+
puts ""
|
|
241
|
+
puts "🔍 Running analyzers...".color(:cyan)
|
|
242
|
+
|
|
243
|
+
@options[:analyzers].each do |analyzer|
|
|
244
|
+
puts " Running #{analyzer} analyzer..."
|
|
245
|
+
case analyzer
|
|
246
|
+
when "controllers"
|
|
247
|
+
results = RailsForge::Analyzers::ControllerAnalyzer.analyze
|
|
248
|
+
RailsForge::Analyzers::ControllerAnalyzer.print_results(results)
|
|
249
|
+
when "models"
|
|
250
|
+
results = RailsForge::Analyzers::ModelAnalyzer.analyze
|
|
251
|
+
RailsForge::Analyzers::ModelAnalyzer.print_results(results)
|
|
252
|
+
when "specs"
|
|
253
|
+
results = RailsForge::Analyzers::SpecAnalyzer.analyze
|
|
254
|
+
RailsForge::Analyzers::SpecAnalyzer.print_results(results)
|
|
255
|
+
when "database"
|
|
256
|
+
results = RailsForge::Analyzers::DatabaseAnalyzer.analyze
|
|
257
|
+
RailsForge::Analyzers::DatabaseAnalyzer.print_report(results)
|
|
258
|
+
when "metrics"
|
|
259
|
+
results = RailsForge::Analyzers::MetricsAnalyzer.analyze
|
|
260
|
+
RailsForge::Analyzers::MetricsAnalyzer.print_results(results)
|
|
261
|
+
when "security"
|
|
262
|
+
results = RailsForge::Analyzers::SecurityAnalyzer.analyze
|
|
263
|
+
RailsForge::Analyzers::SecurityAnalyzer.print_report(results)
|
|
264
|
+
when "performance"
|
|
265
|
+
results = RailsForge::Analyzers::PerformanceAnalyzer.analyze
|
|
266
|
+
RailsForge::Analyzers::PerformanceAnalyzer.print_report(results)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Print final results
|
|
272
|
+
def print_results
|
|
273
|
+
puts ""
|
|
274
|
+
puts "✅ Generation complete!".color(:green)
|
|
275
|
+
puts ""
|
|
276
|
+
puts "Project '#{@options[:project_name]}' created successfully!"
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Add color support for String class
|
|
282
|
+
class String
|
|
283
|
+
def color(color_code)
|
|
284
|
+
"\e[#{color_code}m#{self}\e[0m"
|
|
285
|
+
end
|
|
286
|
+
end
|
data/lib/railsforge.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# RailsForge gem entry point
|
|
2
|
+
# This file is the main entry point for the gem
|
|
3
|
+
|
|
4
|
+
require_relative "railsforge/loader"
|
|
5
|
+
|
|
6
|
+
# The main RailsForge module is defined in loader.rb
|
|
7
|
+
# This file exists for backward compatibility
|
|
8
|
+
module RailsForge
|
|
9
|
+
# Greeting method - kept for compatibility
|
|
10
|
+
def self.greet
|
|
11
|
+
"Welcome to RailsForge #{VERSION}!"
|
|
12
|
+
end
|
|
13
|
+
end
|