belt 0.1.6 → 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/cli/app_detection.rb +6 -0
- data/lib/belt/cli/deploy_command.rb +15 -0
- 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 +48 -18
- data/lib/belt/cli/new_command.rb +1 -1
- data/lib/belt/cli/routes_command/route_inference.rb +4 -0
- data/lib/belt/cli.rb +24 -3
- data/lib/belt/version.rb +1 -1
- data/lib/templates/environment/outputs.tf.erb +5 -0
- data/lib/templates/frontend_infra/frontend.tf.erb +0 -4
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 479b619266e398d1d3052c96eb18f05c8452306a01aa46576b82e072372820b5
|
|
4
|
+
data.tar.gz: d5697750193b1a1cbba8d39e81190035d8fd40db490efbdb24903e7647374767
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 73ac84ffd0e2238b929180f89972f70c55a7d43a92d0f9de23b85cc6c318982c40c9466f49fe1d2d0960a47685acc90e1352b43f7e390ffee2951c83568ba432
|
|
7
|
+
data.tar.gz: 74db2374a336ba5ac774a5a8a4f7c053136de9dc652907e4abeebd69ec6bfa6485f35d3339787e7e5b17feadb3cf23014ae356d8050e817c90a2ba1fbf817ba6
|
|
@@ -28,6 +28,12 @@ module Belt
|
|
|
28
28
|
File.basename(Dir.pwd)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# Detects existing environments by scanning infrastructure/ directories.
|
|
32
|
+
# Each subdirectory with a main.tf is considered an environment.
|
|
33
|
+
def detect_environments
|
|
34
|
+
Dir.glob('infrastructure/*/main.tf').map { |f| File.basename(File.dirname(f)) }.sort
|
|
35
|
+
end
|
|
36
|
+
|
|
31
37
|
# S3 bucket names follow DNS rules — underscores are not allowed.
|
|
32
38
|
# App names often use underscores (Ruby namespaces); convert for S3.
|
|
33
39
|
def s3_safe_name(name)
|
|
@@ -127,6 +127,9 @@ module Belt
|
|
|
127
127
|
|
|
128
128
|
puts "\n✅ Deployed #{@env} successfully!"
|
|
129
129
|
print_outputs(env_dir)
|
|
130
|
+
|
|
131
|
+
deploy_frontend_if_exists
|
|
132
|
+
|
|
130
133
|
puts "\n Run `belt server` to view your app locally (auto-connects to the deployed API)."
|
|
131
134
|
end
|
|
132
135
|
|
|
@@ -479,6 +482,18 @@ module Belt
|
|
|
479
482
|
File.delete('tfplan') if File.exist?('tfplan')
|
|
480
483
|
end
|
|
481
484
|
|
|
485
|
+
def deploy_frontend_if_exists
|
|
486
|
+
frontend_dir = File.join(@project_root, 'frontend')
|
|
487
|
+
return unless Dir.exist?(frontend_dir) && File.exist?(File.join(frontend_dir, 'package.json'))
|
|
488
|
+
|
|
489
|
+
puts "\n━━━ frontend deploy ━━━"
|
|
490
|
+
require_relative 'frontend_deploy_command'
|
|
491
|
+
Belt::CLI::FrontendDeployCommand.new(@env).run
|
|
492
|
+
rescue StandardError => e
|
|
493
|
+
puts "\n ⚠ Frontend deploy failed: #{e.message}"
|
|
494
|
+
puts " Run `belt deploy frontend #{@env}` manually to retry."
|
|
495
|
+
end
|
|
496
|
+
|
|
482
497
|
def print_outputs(env_dir)
|
|
483
498
|
Dir.chdir(env_dir) do
|
|
484
499
|
output = `terraform output 2>/dev/null`.strip
|
|
@@ -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
|
|
@@ -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
|
@@ -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
|
|
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
|
data/lib/belt/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: belt
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stowzilla
|
|
@@ -59,6 +59,7 @@ files:
|
|
|
59
59
|
- lib/belt/cli/bucket_security.rb
|
|
60
60
|
- lib/belt/cli/console_command.rb
|
|
61
61
|
- lib/belt/cli/deploy_command.rb
|
|
62
|
+
- lib/belt/cli/destroy_command.rb
|
|
62
63
|
- lib/belt/cli/env_resolver.rb
|
|
63
64
|
- lib/belt/cli/environment_command.rb
|
|
64
65
|
- lib/belt/cli/frontend_command.rb
|