belt 0.1.5 → 0.1.7
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 +4 -4
- data/lib/belt/assets/belt-default.jpg +0 -0
- data/lib/belt/assets/welcome.css +1 -1
- data/lib/belt/cli/app_detection.rb +23 -1
- data/lib/belt/cli/deploy_command.rb +334 -6
- data/lib/belt/cli/destroy_command.rb +287 -0
- data/lib/belt/cli/environment_command.rb +11 -0
- data/lib/belt/cli/frontend_command.rb +35 -3
- data/lib/belt/cli/frontend_setup_command.rb +5 -2
- data/lib/belt/cli/generate_command.rb +49 -19
- data/lib/belt/cli/new_command.rb +5 -5
- data/lib/belt/cli/routes_command/route_inference.rb +4 -0
- data/lib/belt/cli/server_command.rb +3 -3
- data/lib/belt/cli.rb +24 -3
- data/lib/belt/controllers/welcome_controller.rb +9 -65
- data/lib/belt/rendering.rb +136 -0
- data/lib/belt/version.rb +1 -1
- data/lib/belt/views/welcome/show.html.erb +51 -0
- data/lib/belt.rb +1 -0
- data/lib/belt_controller/base.rb +2 -0
- data/lib/templates/environment/main.tf.erb +16 -3
- data/lib/templates/environment/outputs.tf.erb +5 -0
- data/lib/templates/frontend_infra/frontend.tf.erb +0 -4
- data/lib/templates/new_app/infrastructure/routes.tf.rb.erb +1 -1
- data/lib/templates/new_app/lambda/api.rb.erb +2 -2
- data/lib/templates/new_app/lambda/controllers/application_controller.rb.erb +1 -1
- data/lib/templates/new_app/lambda/lib/routes/routes.rb.erb +1 -1
- metadata +5 -2
- data/lib/belt/assets/belt-default.png +0 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require_relative 'app_detection'
|
|
5
|
+
|
|
6
|
+
module Belt
|
|
7
|
+
module CLI
|
|
8
|
+
class DestroyCommand
|
|
9
|
+
GENERATORS = %w[scaffold resource model controller environment frontend views].freeze
|
|
10
|
+
|
|
11
|
+
include AppDetection
|
|
12
|
+
|
|
13
|
+
def self.run(args)
|
|
14
|
+
generator = args.shift
|
|
15
|
+
|
|
16
|
+
if generator.nil? || generator =~ /\A-/
|
|
17
|
+
print_help
|
|
18
|
+
exit 0
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
unless GENERATORS.include?(generator)
|
|
22
|
+
puts "Unknown generator: '#{generator}'"
|
|
23
|
+
puts "Available: #{GENERATORS.uniq.join(', ')}"
|
|
24
|
+
puts "\nRun 'belt destroy --help' for usage information."
|
|
25
|
+
exit 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Normalize: resource is an alias for scaffold
|
|
29
|
+
generator = 'scaffold' if generator == 'resource'
|
|
30
|
+
|
|
31
|
+
case generator
|
|
32
|
+
when 'environment'
|
|
33
|
+
name = args.shift
|
|
34
|
+
if name.nil? || name.empty?
|
|
35
|
+
puts 'Usage: belt destroy environment <name>'
|
|
36
|
+
exit 1
|
|
37
|
+
end
|
|
38
|
+
new(generator, name, []).destroy
|
|
39
|
+
when 'frontend'
|
|
40
|
+
new(generator, nil, []).destroy
|
|
41
|
+
when 'views'
|
|
42
|
+
name = args.shift
|
|
43
|
+
if name.nil? || name.empty?
|
|
44
|
+
puts 'Usage: belt destroy views <name>'
|
|
45
|
+
exit 1
|
|
46
|
+
end
|
|
47
|
+
new(generator, name, args.map { |a| parse_field(a) }).destroy
|
|
48
|
+
else
|
|
49
|
+
name = args.shift
|
|
50
|
+
if name.nil? || name.empty?
|
|
51
|
+
puts "Usage: belt destroy #{generator} <name>"
|
|
52
|
+
exit 1
|
|
53
|
+
end
|
|
54
|
+
new(generator, name, args.map { |a| parse_field(a) }).destroy
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.parse_field(arg)
|
|
59
|
+
name, type = arg.split(':', 2)
|
|
60
|
+
{ name: name, type: type || 'string' }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.print_help
|
|
64
|
+
puts <<~HELP
|
|
65
|
+
Usage: belt destroy <generator> <name> [field:type ...]
|
|
66
|
+
belt d <generator> <name> [field:type ...]
|
|
67
|
+
|
|
68
|
+
Removes files and references created by `belt generate`.
|
|
69
|
+
|
|
70
|
+
Generators:
|
|
71
|
+
scaffold Remove model, controller, routes, schema, and views
|
|
72
|
+
resource Alias for scaffold
|
|
73
|
+
model Remove an ActiveItem model
|
|
74
|
+
controller Remove a controller
|
|
75
|
+
environment Remove a deployment environment directory
|
|
76
|
+
frontend Remove the frontend/ directory
|
|
77
|
+
views Remove React pages for a resource
|
|
78
|
+
|
|
79
|
+
Examples:
|
|
80
|
+
belt d scaffold post title:string body:text status:string
|
|
81
|
+
belt d model user
|
|
82
|
+
belt d controller comments
|
|
83
|
+
belt d environment staging
|
|
84
|
+
belt d frontend
|
|
85
|
+
belt d views post
|
|
86
|
+
|
|
87
|
+
⚠ This is destructive. Files will be permanently deleted.
|
|
88
|
+
HELP
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def initialize(generator, name, fields)
|
|
92
|
+
@generator = generator
|
|
93
|
+
@name = name&.downcase&.gsub(/[^a-z0-9_]/, '_')
|
|
94
|
+
@fields = fields
|
|
95
|
+
@app_name = detect_namespace
|
|
96
|
+
@resource_name = if @name
|
|
97
|
+
@name.end_with?('s') ? @name : "#{@name}s"
|
|
98
|
+
end
|
|
99
|
+
@singular_name = if @name
|
|
100
|
+
@name.end_with?('s') ? @name.chomp('s') : @name
|
|
101
|
+
end
|
|
102
|
+
@class_name = @singular_name&.split('_')&.map(&:capitalize)&.join
|
|
103
|
+
@removed = []
|
|
104
|
+
@updated = []
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def destroy
|
|
108
|
+
case @generator
|
|
109
|
+
when 'scaffold' then destroy_scaffold
|
|
110
|
+
when 'model' then destroy_model
|
|
111
|
+
when 'controller' then destroy_controller
|
|
112
|
+
when 'environment' then destroy_environment
|
|
113
|
+
when 'frontend' then destroy_frontend
|
|
114
|
+
when 'views' then destroy_views
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
print_summary
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def destroy_scaffold
|
|
123
|
+
destroy_model
|
|
124
|
+
destroy_controller
|
|
125
|
+
remove_routes
|
|
126
|
+
remove_schema
|
|
127
|
+
destroy_views
|
|
128
|
+
puts "\n✓ Scaffold '#{@singular_name}' destroyed!"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def destroy_model
|
|
132
|
+
path = "lambda/models/#{@singular_name}.rb"
|
|
133
|
+
remove_file(path)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def destroy_controller
|
|
137
|
+
path = "lambda/controllers/#{@app_name}/#{@resource_name}_controller.rb"
|
|
138
|
+
remove_file(path)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def destroy_environment
|
|
142
|
+
dir = "infrastructure/#{@name}"
|
|
143
|
+
if Dir.exist?(dir)
|
|
144
|
+
FileUtils.rm_rf(dir)
|
|
145
|
+
@removed << dir
|
|
146
|
+
puts " remove #{dir}/"
|
|
147
|
+
puts "\n✓ Environment '#{@name}' destroyed!"
|
|
148
|
+
else
|
|
149
|
+
puts "✗ Environment '#{@name}' not found at #{dir}/"
|
|
150
|
+
exit 1
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def destroy_frontend
|
|
155
|
+
dir = 'frontend'
|
|
156
|
+
if Dir.exist?(dir)
|
|
157
|
+
FileUtils.rm_rf(dir)
|
|
158
|
+
@removed << dir
|
|
159
|
+
puts " remove #{dir}/"
|
|
160
|
+
puts "\n✓ Frontend removed!"
|
|
161
|
+
else
|
|
162
|
+
puts '✗ No frontend/ directory found.'
|
|
163
|
+
exit 1
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def destroy_views
|
|
168
|
+
return unless @resource_name
|
|
169
|
+
|
|
170
|
+
pages_dir = "frontend/src/pages/#{@resource_name}"
|
|
171
|
+
if Dir.exist?(pages_dir)
|
|
172
|
+
FileUtils.rm_rf(pages_dir)
|
|
173
|
+
@removed << pages_dir
|
|
174
|
+
puts " remove #{pages_dir}/"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
remove_view_routes
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def remove_file(path)
|
|
181
|
+
if File.exist?(path)
|
|
182
|
+
File.delete(path)
|
|
183
|
+
@removed << path
|
|
184
|
+
puts " remove #{path}"
|
|
185
|
+
|
|
186
|
+
# Clean up empty parent directories
|
|
187
|
+
dir = File.dirname(path)
|
|
188
|
+
while dir != '.' && Dir.exist?(dir) && Dir.empty?(dir)
|
|
189
|
+
Dir.rmdir(dir)
|
|
190
|
+
puts " remove #{dir}/"
|
|
191
|
+
dir = File.dirname(dir)
|
|
192
|
+
end
|
|
193
|
+
else
|
|
194
|
+
puts " skip #{path} (not found)"
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def remove_routes
|
|
199
|
+
routes_file = 'infrastructure/routes.tf.rb'
|
|
200
|
+
return unless File.exist?(routes_file)
|
|
201
|
+
|
|
202
|
+
content = File.read(routes_file)
|
|
203
|
+
original = content.dup
|
|
204
|
+
|
|
205
|
+
# Remove "resources :posts" or "resources :posts, tables: [...]" line
|
|
206
|
+
content.gsub!(/^\s*resources :#{@resource_name}.*\n/, '')
|
|
207
|
+
|
|
208
|
+
if content != original
|
|
209
|
+
File.write(routes_file, content)
|
|
210
|
+
@updated << routes_file
|
|
211
|
+
puts " update #{routes_file}"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Update route manifest
|
|
215
|
+
remove_route_manifest_entries
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def remove_route_manifest_entries
|
|
219
|
+
manifest_file = "lambda/lib/routes/#{@app_name}_routes.rb"
|
|
220
|
+
return unless File.exist?(manifest_file)
|
|
221
|
+
|
|
222
|
+
content = File.read(manifest_file)
|
|
223
|
+
original = content.dup
|
|
224
|
+
|
|
225
|
+
# Remove route entries for this controller
|
|
226
|
+
content.gsub!(/^\s*\{ verb: .+?controller: '#{@resource_name}'.+?\},?\n/, '')
|
|
227
|
+
|
|
228
|
+
# Clean up trailing commas and empty arrays
|
|
229
|
+
content.gsub!(/,(\s*\n\s*\]\.freeze)/, '\1')
|
|
230
|
+
|
|
231
|
+
if content != original
|
|
232
|
+
File.write(manifest_file, content)
|
|
233
|
+
@updated << manifest_file
|
|
234
|
+
puts " update #{manifest_file}"
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def remove_schema
|
|
239
|
+
schema_file = 'infrastructure/schema.tf.rb'
|
|
240
|
+
return unless File.exist?(schema_file)
|
|
241
|
+
|
|
242
|
+
content = File.read(schema_file)
|
|
243
|
+
original = content.dup
|
|
244
|
+
|
|
245
|
+
# Remove the model block for this resource
|
|
246
|
+
content.gsub!(/\n?\s*model :#{@singular_name} do\n.*?\n\s*end\n?/m, "\n")
|
|
247
|
+
|
|
248
|
+
# Clean up excessive blank lines
|
|
249
|
+
content.gsub!(/\n{3,}/, "\n\n")
|
|
250
|
+
|
|
251
|
+
if content != original
|
|
252
|
+
File.write(schema_file, content)
|
|
253
|
+
@updated << schema_file
|
|
254
|
+
puts " update #{schema_file}"
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def remove_view_routes
|
|
259
|
+
app_jsx = 'frontend/src/App.jsx'
|
|
260
|
+
return unless File.exist?(app_jsx)
|
|
261
|
+
|
|
262
|
+
content = File.read(app_jsx)
|
|
263
|
+
original = content.dup
|
|
264
|
+
|
|
265
|
+
# Remove import lines for this resource's pages
|
|
266
|
+
content.gsub!(/^import #{@class_name}\w* from '.\/pages\/#{@resource_name}\/.*'\n/, '')
|
|
267
|
+
|
|
268
|
+
# Remove Route elements for this resource
|
|
269
|
+
content.gsub!(/^\s*<Route path="\/#{@resource_name}.*?\/>\n/, '')
|
|
270
|
+
|
|
271
|
+
if content != original
|
|
272
|
+
File.write(app_jsx, content)
|
|
273
|
+
@updated << app_jsx
|
|
274
|
+
puts " update #{app_jsx}"
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def print_summary
|
|
279
|
+
return if @removed.empty? && @updated.empty?
|
|
280
|
+
|
|
281
|
+
puts "\nSummary:"
|
|
282
|
+
puts " Removed: #{@removed.length} file(s)/dir(s)" unless @removed.empty?
|
|
283
|
+
puts " Updated: #{@updated.length} file(s)" unless @updated.empty?
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
@@ -51,6 +51,8 @@ module Belt
|
|
|
51
51
|
|
|
52
52
|
puts "\n✓ Environment '#{@env_name}' created!"
|
|
53
53
|
|
|
54
|
+
setup_frontend_if_exists
|
|
55
|
+
|
|
54
56
|
unless @quiet
|
|
55
57
|
puts "\nNext steps:"
|
|
56
58
|
puts " cd #{dest_dir}"
|
|
@@ -62,6 +64,15 @@ module Belt
|
|
|
62
64
|
|
|
63
65
|
private
|
|
64
66
|
|
|
67
|
+
def setup_frontend_if_exists
|
|
68
|
+
return unless Dir.exist?('frontend') && !Dir.empty?('frontend')
|
|
69
|
+
|
|
70
|
+
puts "\n Frontend detected — generating frontend infrastructure..."
|
|
71
|
+
require_relative 'frontend_setup_command'
|
|
72
|
+
FrontendSetupCommand.new(@env_name, quiet: true).run
|
|
73
|
+
puts " create infrastructure/#{@env_name}/frontend.tf"
|
|
74
|
+
end
|
|
75
|
+
|
|
65
76
|
def templates
|
|
66
77
|
{
|
|
67
78
|
'main.tf.erb' => 'main.tf',
|
|
@@ -53,14 +53,46 @@ module Belt
|
|
|
53
53
|
copy_template(framework_dir, dest_dir)
|
|
54
54
|
|
|
55
55
|
puts "\n✓ Frontend (#{@framework}) created in frontend/"
|
|
56
|
+
|
|
57
|
+
install_dependencies(dest_dir)
|
|
58
|
+
setup_frontend_infra_for_existing_environments
|
|
59
|
+
|
|
56
60
|
puts "\nNext steps:"
|
|
57
|
-
puts '
|
|
58
|
-
puts ' belt
|
|
59
|
-
puts ' belt deploy frontend <env> # Build and deploy to AWS'
|
|
61
|
+
puts ' belt server # Start local dev server'
|
|
62
|
+
puts ' belt deploy # Deploy everything to AWS'
|
|
60
63
|
end
|
|
61
64
|
|
|
62
65
|
private
|
|
63
66
|
|
|
67
|
+
def install_dependencies(dest_dir)
|
|
68
|
+
puts "\n Installing npm dependencies..."
|
|
69
|
+
success = system('npm', 'install', '--prefix', dest_dir, '--loglevel', 'error')
|
|
70
|
+
if success
|
|
71
|
+
puts " ✓ Dependencies installed"
|
|
72
|
+
else
|
|
73
|
+
puts " ⚠ npm install failed — run `cd #{dest_dir} && npm install` manually"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def setup_frontend_infra_for_existing_environments
|
|
78
|
+
environments = detect_environments
|
|
79
|
+
return if environments.empty?
|
|
80
|
+
|
|
81
|
+
puts "\n Setting up frontend infrastructure for existing environments..."
|
|
82
|
+
require_relative 'frontend_setup_command'
|
|
83
|
+
|
|
84
|
+
environments.each do |env_name|
|
|
85
|
+
frontend_tf = "infrastructure/#{env_name}/frontend.tf"
|
|
86
|
+
if File.exist?(frontend_tf)
|
|
87
|
+
puts " skip #{frontend_tf} (already exists)"
|
|
88
|
+
next
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
FrontendSetupCommand.new(env_name, quiet: true).run
|
|
92
|
+
puts " create #{frontend_tf}"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
64
96
|
def copy_template(src_dir, dest_dir)
|
|
65
97
|
Dir.glob("#{src_dir}/**/*", File::FNM_DOTMATCH).each do |src|
|
|
66
98
|
next if File.directory?(src)
|
|
@@ -29,15 +29,18 @@ module Belt
|
|
|
29
29
|
new(env).run
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
def initialize(env)
|
|
32
|
+
def initialize(env, quiet: false)
|
|
33
33
|
@env = env
|
|
34
34
|
@app_name = detect_app_name
|
|
35
35
|
@env_dir = "infrastructure/#{@env}"
|
|
36
|
+
@quiet = quiet
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
def run
|
|
39
40
|
validate!
|
|
40
41
|
generate_frontend_tf
|
|
42
|
+
return if @quiet
|
|
43
|
+
|
|
41
44
|
puts "\n✓ Frontend infrastructure generated for '#{@env}'!"
|
|
42
45
|
puts "\nRun `belt apply #{@env}` to create the S3 bucket and CloudFront distribution."
|
|
43
46
|
puts "Then `belt deploy frontend #{@env}` to build and deploy."
|
|
@@ -57,7 +60,7 @@ module Belt
|
|
|
57
60
|
template_path = File.join(TEMPLATE_DIR, 'frontend.tf.erb')
|
|
58
61
|
content = ERB.new(File.read(template_path), trim_mode: '-').result(binding)
|
|
59
62
|
File.write(dest, content)
|
|
60
|
-
puts " create #{dest}"
|
|
63
|
+
puts " create #{dest}" unless @quiet
|
|
61
64
|
end
|
|
62
65
|
end
|
|
63
66
|
end
|
|
@@ -11,23 +11,23 @@ module Belt
|
|
|
11
11
|
module CLI
|
|
12
12
|
class GenerateCommand
|
|
13
13
|
TEMPLATE_DIR = File.expand_path('../../templates/generate', __dir__)
|
|
14
|
-
GENERATORS = %w[resource model controller environment frontend views].freeze
|
|
14
|
+
GENERATORS = %w[scaffold resource model controller environment frontend views].freeze
|
|
15
15
|
|
|
16
16
|
include AppDetection
|
|
17
17
|
|
|
18
18
|
FIELD_TYPES = %w[string text integer float boolean date datetime].freeze
|
|
19
19
|
|
|
20
20
|
GENERATOR_HELP = {
|
|
21
|
-
'
|
|
22
|
-
description: 'Generate a model, controller, routes, and
|
|
23
|
-
usage: 'belt generate
|
|
21
|
+
'scaffold' => {
|
|
22
|
+
description: 'Generate a model, controller, routes, schema, and views for a REST resource.',
|
|
23
|
+
usage: 'belt generate scaffold <name> [field:type ...] [options]',
|
|
24
24
|
options: [
|
|
25
25
|
['--skip-views', 'Skip generating frontend view pages']
|
|
26
26
|
],
|
|
27
27
|
examples: [
|
|
28
|
-
['belt g
|
|
29
|
-
['belt g
|
|
30
|
-
['belt g
|
|
28
|
+
['belt g scaffold post title:string body:text', 'Post with title and body fields'],
|
|
29
|
+
['belt g scaffold comment author:string body:text status:string', 'Scaffold with multiple fields'],
|
|
30
|
+
['belt g scaffold task --skip-views', 'Scaffold without frontend pages']
|
|
31
31
|
],
|
|
32
32
|
notes: <<~NOTES
|
|
33
33
|
Creates:
|
|
@@ -36,6 +36,9 @@ module Belt
|
|
|
36
36
|
infrastructure/routes.tf.rb Route entry added
|
|
37
37
|
infrastructure/schema.tf.rb DynamoDB table schema added
|
|
38
38
|
lambda/lib/routes/<app>_routes.rb Route manifest updated
|
|
39
|
+
frontend/src/pages/<names>/ React pages (if frontend exists)
|
|
40
|
+
|
|
41
|
+
Alias: `belt g resource` works the same way.
|
|
39
42
|
NOTES
|
|
40
43
|
},
|
|
41
44
|
'model' => {
|
|
@@ -77,12 +80,15 @@ module Belt
|
|
|
77
80
|
|
|
78
81
|
unless GENERATORS.include?(generator)
|
|
79
82
|
puts "Unknown generator: '#{generator}'"
|
|
80
|
-
puts "Available generators: #{GENERATORS.join(', ')}"
|
|
83
|
+
puts "Available generators: #{(GENERATORS - ['resource']).join(', ')}"
|
|
81
84
|
puts "\nRun 'belt generate --help' for usage information."
|
|
82
85
|
exit 1
|
|
83
86
|
end
|
|
84
87
|
|
|
85
|
-
#
|
|
88
|
+
# Normalize: resource is an alias for scaffold
|
|
89
|
+
generator = 'scaffold' if generator == 'resource'
|
|
90
|
+
|
|
91
|
+
# Per-generator help: belt g scaffold --help
|
|
86
92
|
if args.include?('--help') || args.include?('-h')
|
|
87
93
|
print_generator_help(generator)
|
|
88
94
|
exit 0
|
|
@@ -143,18 +149,21 @@ module Belt
|
|
|
143
149
|
belt g <generator> <name> [field:type ...] [options]
|
|
144
150
|
|
|
145
151
|
Generators:
|
|
146
|
-
|
|
152
|
+
scaffold Generate model, controller, routes, schema, and views (full REST resource)
|
|
147
153
|
model Generate an ActiveItem model
|
|
148
154
|
controller Generate a controller
|
|
149
155
|
environment Create a new deployment environment
|
|
150
156
|
frontend Scaffold a frontend app (react, vue, svelte)
|
|
151
157
|
views Generate React pages for a resource
|
|
152
158
|
|
|
159
|
+
Aliases:
|
|
160
|
+
resource Same as scaffold
|
|
161
|
+
|
|
153
162
|
Field Types:
|
|
154
163
|
#{FIELD_TYPES.join(', ')}
|
|
155
164
|
|
|
156
165
|
Examples:
|
|
157
|
-
belt g
|
|
166
|
+
belt g scaffold post title:string body:text status:string
|
|
158
167
|
belt g model user email:string name:string
|
|
159
168
|
belt g controller comments
|
|
160
169
|
belt g environment staging
|
|
@@ -210,7 +219,7 @@ module Belt
|
|
|
210
219
|
@name = name.downcase.gsub(/[^a-z0-9_]/, '_')
|
|
211
220
|
@fields = fields
|
|
212
221
|
@skip_views = skip_views
|
|
213
|
-
@app_name =
|
|
222
|
+
@app_name = detect_namespace
|
|
214
223
|
@module_name = @app_name.split(/[-_]/).map(&:capitalize).join
|
|
215
224
|
@resource_name = @name.end_with?('s') ? @name : "#{@name}s"
|
|
216
225
|
@singular_name = @name.end_with?('s') ? @name.chomp('s') : @name
|
|
@@ -219,7 +228,7 @@ module Belt
|
|
|
219
228
|
|
|
220
229
|
def generate
|
|
221
230
|
case @generator
|
|
222
|
-
when '
|
|
231
|
+
when 'scaffold' then generate_resource
|
|
223
232
|
when 'model' then generate_model
|
|
224
233
|
when 'controller' then generate_controller
|
|
225
234
|
end
|
|
@@ -233,7 +242,7 @@ module Belt
|
|
|
233
242
|
inject_routes
|
|
234
243
|
inject_schema
|
|
235
244
|
generate_views_if_frontend
|
|
236
|
-
puts "\n✓
|
|
245
|
+
puts "\n✓ Scaffold '#{@singular_name}' generated!"
|
|
237
246
|
puts "\nFiles created/updated:"
|
|
238
247
|
puts " lambda/models/#{@singular_name}.rb"
|
|
239
248
|
puts " lambda/controllers/#{@app_name}/#{@resource_name}_controller.rb"
|
|
@@ -261,14 +270,35 @@ module Belt
|
|
|
261
270
|
|
|
262
271
|
content = File.read(routes_file)
|
|
263
272
|
tables_arg = @fields.any? ? ", tables: [:#{@resource_name}]" : ''
|
|
273
|
+
resource_line = "resources :#{@resource_name}#{tables_arg}"
|
|
264
274
|
|
|
265
|
-
#
|
|
275
|
+
# Replace the commented placeholder if it exists
|
|
266
276
|
if content.include?('# resources :posts')
|
|
267
|
-
content.sub!('# resources :posts',
|
|
268
|
-
elsif content.match?(/namespace :\w+[^\n]*do\n(\s+#[^\n]*\n)*\s+end/)
|
|
269
|
-
content.sub!(/^(\s+)(end\s*\z)/m, "\\1 resources :#{@resource_name}#{tables_arg}\n\\1\\2")
|
|
277
|
+
content.sub!('# resources :posts', resource_line)
|
|
270
278
|
else
|
|
271
|
-
|
|
279
|
+
# Find the target namespace block and insert before its closing `end`
|
|
280
|
+
# Match: `namespace :<name> do` ... `end` (the first `end` at the same indent level)
|
|
281
|
+
namespace_pattern = /^(\s*)namespace :#{Regexp.escape(@app_name)}\b[^\n]*do\s*\n(.*?)^\1end/m
|
|
282
|
+
|
|
283
|
+
if content.match?(namespace_pattern)
|
|
284
|
+
content.sub!(namespace_pattern) do |match|
|
|
285
|
+
indent = $1
|
|
286
|
+
# Insert the resource line before the namespace's closing `end`
|
|
287
|
+
match.sub(/^(#{indent})end\z/m, "#{indent} #{resource_line}\n#{indent}end")
|
|
288
|
+
end
|
|
289
|
+
else
|
|
290
|
+
# Fallback: find any single namespace block and insert into it
|
|
291
|
+
single_ns_pattern = /^(\s*)namespace :\w+\b[^\n]*do\s*\n(.*?)^\1end/m
|
|
292
|
+
if content.match?(single_ns_pattern)
|
|
293
|
+
content.sub!(single_ns_pattern) do |match|
|
|
294
|
+
indent = $1
|
|
295
|
+
match.sub(/^(#{indent})end\z/m, "#{indent} #{resource_line}\n#{indent}end")
|
|
296
|
+
end
|
|
297
|
+
else
|
|
298
|
+
# No namespace block found at all — insert at top level before final end
|
|
299
|
+
content.sub!(/^(\s*end\s*)\z/m, " #{resource_line}\n\\1")
|
|
300
|
+
end
|
|
301
|
+
end
|
|
272
302
|
end
|
|
273
303
|
|
|
274
304
|
File.write(routes_file, content)
|
data/lib/belt/cli/new_command.rb
CHANGED
|
@@ -82,8 +82,8 @@ module Belt
|
|
|
82
82
|
|
|
83
83
|
puts "Creating new Belt application: #{@app_name}"
|
|
84
84
|
create_structure
|
|
85
|
-
generate_frontend if @frontend
|
|
86
85
|
generate_environments
|
|
86
|
+
generate_frontend if @frontend
|
|
87
87
|
init_git
|
|
88
88
|
run_bundle_install
|
|
89
89
|
setup_state
|
|
@@ -107,7 +107,7 @@ module Belt
|
|
|
107
107
|
|
|
108
108
|
def directories
|
|
109
109
|
%W[
|
|
110
|
-
#{@app_name}/lambda/controllers
|
|
110
|
+
#{@app_name}/lambda/controllers/api
|
|
111
111
|
#{@app_name}/lambda/models
|
|
112
112
|
#{@app_name}/lambda/models/concerns
|
|
113
113
|
#{@app_name}/lambda/lib/routes
|
|
@@ -121,13 +121,13 @@ module Belt
|
|
|
121
121
|
{
|
|
122
122
|
'Gemfile.erb' => "#{@app_name}/Gemfile",
|
|
123
123
|
'Rakefile.erb' => "#{@app_name}/Rakefile",
|
|
124
|
-
'lambda/api.rb.erb' => "#{@app_name}/lambda
|
|
124
|
+
'lambda/api.rb.erb' => "#{@app_name}/lambda/api.rb",
|
|
125
125
|
'lambda/config/environment.rb.erb' => "#{@app_name}/lambda/config/environment.rb",
|
|
126
126
|
'lambda/models/application_record.rb.erb' => "#{@app_name}/lambda/models/application_record.rb",
|
|
127
127
|
'lambda/models/concerns/timestampable.rb.erb' => "#{@app_name}/lambda/models/concerns/timestampable.rb",
|
|
128
128
|
'lambda/controllers/application_controller.rb.erb' =>
|
|
129
|
-
"#{@app_name}/lambda/controllers
|
|
130
|
-
'lambda/lib/routes/routes.rb.erb' => "#{@app_name}/lambda/lib/routes
|
|
129
|
+
"#{@app_name}/lambda/controllers/api/application_controller.rb",
|
|
130
|
+
'lambda/lib/routes/routes.rb.erb' => "#{@app_name}/lambda/lib/routes/api_routes.rb",
|
|
131
131
|
'infrastructure/routes.tf.rb.erb' => "#{@app_name}/infrastructure/routes.tf.rb",
|
|
132
132
|
'infrastructure/schema.tf.rb.erb' => "#{@app_name}/infrastructure/schema.tf.rb",
|
|
133
133
|
'README.md.erb' => "#{@app_name}/README.md",
|
|
@@ -16,6 +16,10 @@ module Belt
|
|
|
16
16
|
|
|
17
17
|
return non_param.map { |s| s.gsub('-', '_') }.join('/') if route.resource? && nested_resource?(segments)
|
|
18
18
|
|
|
19
|
+
# For non-resource routes with a single segment (e.g., post '/signup' in :onboarding),
|
|
20
|
+
# the segment is the action name, not the controller. Use the gateway name as controller.
|
|
21
|
+
return gateway.name if !route.resource? && non_param.length == 1
|
|
22
|
+
|
|
19
23
|
non_param.first.gsub('-', '_')
|
|
20
24
|
end
|
|
21
25
|
|
|
@@ -144,7 +144,7 @@ module Belt
|
|
|
144
144
|
subtitle = ENV.fetch('WELCOME_SUBTITLE', 'Your app is ready. Deploy to see it live on AWS.')
|
|
145
145
|
|
|
146
146
|
css = load_asset('welcome.css') || ''
|
|
147
|
-
image_b64 = load_image_base64('belt-default.
|
|
147
|
+
image_b64 = load_image_base64('belt-default.jpg') || ''
|
|
148
148
|
|
|
149
149
|
if @api_url
|
|
150
150
|
deployed_html(css: css, image_b64: image_b64)
|
|
@@ -175,7 +175,7 @@ module Belt
|
|
|
175
175
|
</head>
|
|
176
176
|
<body>
|
|
177
177
|
<div class="hero">
|
|
178
|
-
<img src="data:image/
|
|
178
|
+
<img src="data:image/jpeg;base64,#{image_b64}" alt="Belt" class="hero-bg" />
|
|
179
179
|
<div class="overlay">
|
|
180
180
|
<h1>#{@app_name}</h1>
|
|
181
181
|
<p class="subtitle">Deployed and running on AWS</p>
|
|
@@ -208,7 +208,7 @@ module Belt
|
|
|
208
208
|
</head>
|
|
209
209
|
<body>
|
|
210
210
|
<div class="hero">
|
|
211
|
-
<img src="data:image/
|
|
211
|
+
<img src="data:image/jpeg;base64,#{image_b64}" alt="Belt" class="hero-bg" />
|
|
212
212
|
<div class="overlay">
|
|
213
213
|
<h1>#{title}</h1>
|
|
214
214
|
<p class="subtitle">#{subtitle}</p>
|
data/lib/belt/cli.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative 'root'
|
|
|
5
5
|
require_relative 'cli/env_resolver'
|
|
6
6
|
require_relative 'cli/new_command'
|
|
7
7
|
require_relative 'cli/generate_command'
|
|
8
|
+
require_relative 'cli/destroy_command'
|
|
8
9
|
require_relative 'cli/frontend_command'
|
|
9
10
|
require_relative 'cli/frontend_setup_command'
|
|
10
11
|
require_relative 'cli/frontend_deploy_command'
|
|
@@ -22,6 +23,7 @@ module Belt
|
|
|
22
23
|
COMMANDS_DEFINITION = {
|
|
23
24
|
'new' => Belt::CLI::NewCommand,
|
|
24
25
|
%w[generate g] => Belt::CLI::GenerateCommand,
|
|
26
|
+
%w[destroy d] => Belt::CLI::DestroyCommand,
|
|
25
27
|
'routes' => Belt::CLI::RoutesCommand,
|
|
26
28
|
%w[console c] => Belt::CLI::ConsoleCommand,
|
|
27
29
|
%w[tasks --tasks -T] => Belt::CLI::TasksCommand,
|
|
@@ -45,7 +47,21 @@ module Belt
|
|
|
45
47
|
exit 1
|
|
46
48
|
end
|
|
47
49
|
|
|
48
|
-
#
|
|
50
|
+
# `belt destroy` is ambiguous: could be terraform destroy or belt destroy <generator>.
|
|
51
|
+
# Route to DestroyCommand if the first arg is a known generator or help flag.
|
|
52
|
+
if command == 'destroy'
|
|
53
|
+
if args.empty? || args.first =~ /\A-/
|
|
54
|
+
# belt destroy --help → DestroyCommand help
|
|
55
|
+
# belt destroy (no args) → terraform destroy (needs env)
|
|
56
|
+
if args.include?('--help') || args.include?('-h')
|
|
57
|
+
return DestroyCommand.run(args)
|
|
58
|
+
end
|
|
59
|
+
elsif DestroyCommand::GENERATORS.include?(args.first)
|
|
60
|
+
return DestroyCommand.run(args)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Terraform shorthand: belt init wups, belt plan wups, belt apply wups, belt destroy wups
|
|
49
65
|
return Belt::CLI::TerraformCommand.run(command, args) if TERRAFORM_ACTIONS.include?(command)
|
|
50
66
|
|
|
51
67
|
handler = COMMANDS[command]
|
|
@@ -71,10 +87,14 @@ module Belt
|
|
|
71
87
|
|
|
72
88
|
Commands:
|
|
73
89
|
new <app_name> [--frontend react] Create a new Belt application
|
|
74
|
-
generate <
|
|
90
|
+
generate <scaffold|model|controller> <name> Generate components
|
|
75
91
|
generate frontend <react|vue|svelte> Scaffold a frontend app
|
|
76
92
|
generate views <resource> [fields...] Generate React pages for REST actions
|
|
77
93
|
generate environment <name> Create a new environment
|
|
94
|
+
destroy <scaffold|model|controller> <name> Remove generated components
|
|
95
|
+
destroy frontend Remove the frontend/ directory
|
|
96
|
+
destroy views <resource> Remove React pages for a resource
|
|
97
|
+
destroy environment <name> Remove an environment directory
|
|
78
98
|
server Start local dev server (frontend)
|
|
79
99
|
s Alias for server
|
|
80
100
|
deploy [environment] Deploy to AWS (init → plan → apply)
|
|
@@ -106,7 +126,8 @@ module Belt
|
|
|
106
126
|
|
|
107
127
|
Examples:
|
|
108
128
|
belt new blog --frontend react
|
|
109
|
-
belt generate
|
|
129
|
+
belt generate scaffold post title:string content:text status:string
|
|
130
|
+
belt destroy scaffold post
|
|
110
131
|
belt generate frontend react
|
|
111
132
|
belt server # Start local frontend server
|
|
112
133
|
belt deploy # Deploy dev to AWS
|