belt 0.2.17 → 0.3.14
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/CHANGELOG.md +226 -0
- data/README.md +42 -3
- data/lib/belt/action_router.rb +34 -19
- data/lib/belt/cli/app_detection.rb +30 -3
- data/lib/belt/cli/auth_command.rb +395 -0
- data/lib/belt/cli/console_command.rb +17 -2
- data/lib/belt/cli/deploy_command.rb +51 -17
- data/lib/belt/cli/destroy_command.rb +62 -25
- data/lib/belt/cli/doctor_command.rb +166 -22
- data/lib/belt/cli/environment_command.rb +12 -0
- data/lib/belt/cli/explain_command.rb +112 -0
- data/lib/belt/cli/frontend_command.rb +31 -10
- data/lib/belt/cli/frontend_deploy_command.rb +110 -21
- data/lib/belt/cli/frontend_env_command.rb +56 -19
- data/lib/belt/cli/frontend_env_map.rb +30 -10
- data/lib/belt/cli/frontend_registry.rb +373 -0
- data/lib/belt/cli/frontend_setup_command.rb +71 -5
- data/lib/belt/cli/generate_command.rb +61 -42
- data/lib/belt/cli/index_command.rb +192 -0
- data/lib/belt/cli/lambda_config_command.rb +9 -3
- data/lib/belt/cli/routes_command/request_model_inference.rb +131 -0
- data/lib/belt/cli/routes_command/route_inference.rb +4 -3
- data/lib/belt/cli/routes_command.rb +31 -19
- data/lib/belt/cli/server_command.rb +28 -15
- data/lib/belt/cli/tables_command.rb +3 -1
- data/lib/belt/cli/terraform_command.rb +9 -0
- data/lib/belt/cli/views_command.rb +15 -15
- data/lib/belt/cli/zip_artifact_builder.rb +199 -0
- data/lib/belt/cli.rb +17 -9
- data/lib/belt/docs/backups.md +101 -0
- data/lib/belt/docs/console.md +65 -0
- data/lib/belt/docs/controllers.md +155 -0
- data/lib/belt/docs/deployment.md +166 -0
- data/lib/belt/docs/frontend.md +103 -0
- data/lib/belt/docs/generators.md +137 -0
- data/lib/belt/docs/lambda_handler.md +105 -0
- data/lib/belt/docs/models.md +160 -0
- data/lib/belt/docs/observability.md +94 -0
- data/lib/belt/docs/plugins.md +94 -0
- data/lib/belt/docs/routing.md +138 -0
- data/lib/belt/docs/structure.md +96 -0
- data/lib/belt/inflector.rb +8 -0
- data/lib/belt/route_dsl.rb +318 -100
- data/lib/belt/version.rb +1 -1
- data/lib/templates/frontend/react/env.yml.example +2 -2
- data/lib/templates/generate/auth/cognito.tf.erb +71 -0
- data/lib/templates/generate/auth/cognito_outputs.tf.erb +19 -0
- data/lib/templates/generate/auth/frontend/ConfirmEmail.jsx +55 -0
- data/lib/templates/generate/auth/frontend/Login.jsx +79 -0
- data/lib/templates/generate/auth/frontend/ProtectedRoute.jsx +9 -0
- data/lib/templates/generate/auth/frontend/SignUp.jsx +58 -0
- data/lib/templates/generate/auth/frontend/apiClient.js +34 -0
- data/lib/templates/generate/auth/frontend/auth.css +157 -0
- data/lib/templates/generate/auth/frontend/auth.js +121 -0
- data/lib/templates/generate/model.rb.erb +4 -6
- data/lib/templates/module/frontend.tf.erb +49 -43
- data/lib/templates/module/outputs.tf.erb +1 -1
- data/lib/templates/new_app/AGENTS.md.erb +170 -20
- data/lib/templates/new_app/config/lambda/api.yml.erb +1 -0
- data/lib/templates/new_app/config/routes.rb.erb +8 -3
- data/lib/templates/new_app/lambda/api.rb.erb +1 -1
- metadata +56 -1
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
require_relative 'app_detection'
|
|
5
|
+
require_relative 'auth_command'
|
|
6
|
+
require_relative 'frontend_registry'
|
|
5
7
|
require_relative 'generator_registry'
|
|
6
8
|
require_relative 'tables_command'
|
|
7
9
|
require_relative '../inflector'
|
|
@@ -9,7 +11,7 @@ require_relative '../inflector'
|
|
|
9
11
|
module Belt
|
|
10
12
|
module CLI
|
|
11
13
|
class DestroyCommand
|
|
12
|
-
GENERATORS = %w[scaffold resource model controller environment frontend views].freeze
|
|
14
|
+
GENERATORS = %w[scaffold resource model controller environment frontend views index auth].freeze
|
|
13
15
|
|
|
14
16
|
include AppDetection
|
|
15
17
|
|
|
@@ -50,21 +52,28 @@ module Belt
|
|
|
50
52
|
flags = parse_environment_flags(args)
|
|
51
53
|
new(generator, name, [], **flags).destroy
|
|
52
54
|
when 'frontend'
|
|
53
|
-
|
|
55
|
+
frontend_name = FrontendRegistry.extract_flag!(args, '--frontend')
|
|
56
|
+
new(generator, nil, [], frontend_name: frontend_name).destroy
|
|
57
|
+
when 'auth'
|
|
58
|
+
Belt::CLI::AuthCommand.destroy(args)
|
|
54
59
|
when 'views'
|
|
60
|
+
frontend_name = FrontendRegistry.extract_flag!(args, '--frontend')
|
|
55
61
|
name = args.shift
|
|
56
62
|
if name.nil? || name.empty?
|
|
57
|
-
puts 'Usage: belt destroy views <name>'
|
|
63
|
+
puts 'Usage: belt destroy views <name> [--frontend NAME]'
|
|
58
64
|
exit 1
|
|
59
65
|
end
|
|
60
|
-
new(generator, name, args.map { |a| parse_field(a) }).destroy
|
|
66
|
+
new(generator, name, args.map { |a| parse_field(a) }, frontend_name: frontend_name).destroy
|
|
67
|
+
when 'index'
|
|
68
|
+
Belt::CLI::IndexCommand.run(['remove'] + args)
|
|
61
69
|
else
|
|
70
|
+
frontend_name = FrontendRegistry.extract_flag!(args, '--frontend')
|
|
62
71
|
name = args.shift
|
|
63
72
|
if name.nil? || name.empty?
|
|
64
73
|
puts "Usage: belt destroy #{generator} <name>"
|
|
65
74
|
exit 1
|
|
66
75
|
end
|
|
67
|
-
new(generator, name, args.map { |a| parse_field(a) }).destroy
|
|
76
|
+
new(generator, name, args.map { |a| parse_field(a) }, frontend_name: frontend_name).destroy
|
|
68
77
|
end
|
|
69
78
|
end
|
|
70
79
|
|
|
@@ -98,8 +107,9 @@ module Belt
|
|
|
98
107
|
resource Alias for scaffold
|
|
99
108
|
model Remove an ActiveItem model
|
|
100
109
|
controller Remove a controller
|
|
110
|
+
auth Remove Cognito user pool infrastructure
|
|
101
111
|
environment Remove a deployment environment and tear down infrastructure
|
|
102
|
-
frontend Remove
|
|
112
|
+
frontend Remove a frontend directory (use --frontend NAME when several exist)
|
|
103
113
|
views Remove React pages for a resource
|
|
104
114
|
|
|
105
115
|
Environment options:
|
|
@@ -114,7 +124,9 @@ module Belt
|
|
|
114
124
|
belt d environment dev --skip-terraform
|
|
115
125
|
belt d environment dev --force
|
|
116
126
|
belt d frontend
|
|
127
|
+
belt d frontend --frontend ops
|
|
117
128
|
belt d views post
|
|
129
|
+
belt d views post --frontend ops
|
|
118
130
|
|
|
119
131
|
⚠ This is destructive. Files will be permanently deleted.
|
|
120
132
|
|
|
@@ -123,12 +135,14 @@ module Belt
|
|
|
123
135
|
HELP
|
|
124
136
|
end
|
|
125
137
|
|
|
126
|
-
|
|
138
|
+
# rubocop:disable Metrics/ParameterLists -- keyword options for destroy flags
|
|
139
|
+
def initialize(generator, name, fields, force: false, skip_terraform: false, frontend_name: nil)
|
|
127
140
|
@generator = generator
|
|
128
141
|
@name = name&.downcase&.gsub(/[^a-z0-9_]/, '_')
|
|
129
142
|
@fields = fields
|
|
130
143
|
@force = force
|
|
131
144
|
@skip_terraform = skip_terraform
|
|
145
|
+
@frontend_name = frontend_name
|
|
132
146
|
@app_name = detect_namespace
|
|
133
147
|
@singular_name = @name ? Belt::Inflector.singularize(@name) : nil
|
|
134
148
|
@resource_name = @singular_name ? Belt::Inflector.pluralize(@singular_name) : nil
|
|
@@ -136,6 +150,7 @@ module Belt
|
|
|
136
150
|
@removed = []
|
|
137
151
|
@updated = []
|
|
138
152
|
end
|
|
153
|
+
# rubocop:enable Metrics/ParameterLists
|
|
139
154
|
|
|
140
155
|
def destroy
|
|
141
156
|
case @generator
|
|
@@ -344,39 +359,62 @@ module Belt
|
|
|
344
359
|
end
|
|
345
360
|
|
|
346
361
|
def destroy_frontend
|
|
347
|
-
|
|
362
|
+
frontend = FrontendRegistry.new.resolve!(@frontend_name)
|
|
363
|
+
dir = frontend.path
|
|
348
364
|
if Dir.exist?(dir)
|
|
349
365
|
FileUtils.rm_rf(dir)
|
|
350
366
|
@removed << dir
|
|
351
367
|
puts " remove #{dir}/"
|
|
352
368
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
puts " remove #{module_frontend}"
|
|
369
|
+
tf_file = File.join('infrastructure/modules/app', "#{frontend.tf_name}.tf")
|
|
370
|
+
if File.exist?(tf_file)
|
|
371
|
+
FileUtils.rm_f(tf_file)
|
|
372
|
+
@removed << tf_file
|
|
373
|
+
puts " remove #{tf_file}"
|
|
359
374
|
end
|
|
360
375
|
|
|
361
|
-
puts "\n✓
|
|
376
|
+
puts "\n✓ #{frontend.label.capitalize} removed!"
|
|
362
377
|
else
|
|
363
|
-
puts
|
|
378
|
+
puts "✗ No #{dir}/ directory found."
|
|
364
379
|
exit 1
|
|
365
380
|
end
|
|
366
381
|
end
|
|
367
382
|
|
|
383
|
+
def target_frontends
|
|
384
|
+
registry = FrontendRegistry.new
|
|
385
|
+
return [] if registry.empty?
|
|
386
|
+
|
|
387
|
+
if @frontend_name
|
|
388
|
+
[registry.resolve!(@frontend_name)]
|
|
389
|
+
else
|
|
390
|
+
registry.all
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
368
394
|
def destroy_views
|
|
369
395
|
return unless @resource_name
|
|
370
396
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
397
|
+
targets = target_frontends
|
|
398
|
+
if targets.empty?
|
|
399
|
+
pages_dir = "frontend/src/pages/#{@resource_name}"
|
|
400
|
+
if Dir.exist?(pages_dir)
|
|
401
|
+
FileUtils.rm_rf(pages_dir)
|
|
402
|
+
@removed << pages_dir
|
|
403
|
+
puts " remove #{pages_dir}/"
|
|
404
|
+
end
|
|
405
|
+
remove_view_routes_from('frontend/src/App.jsx')
|
|
406
|
+
return
|
|
377
407
|
end
|
|
378
408
|
|
|
379
|
-
|
|
409
|
+
targets.each do |frontend|
|
|
410
|
+
pages_dir = "#{frontend.src_dir}/pages/#{@resource_name}"
|
|
411
|
+
if Dir.exist?(pages_dir)
|
|
412
|
+
FileUtils.rm_rf(pages_dir)
|
|
413
|
+
@removed << pages_dir
|
|
414
|
+
puts " remove #{pages_dir}/"
|
|
415
|
+
end
|
|
416
|
+
remove_view_routes_from(frontend.app_jsx)
|
|
417
|
+
end
|
|
380
418
|
end
|
|
381
419
|
|
|
382
420
|
def remove_file(path)
|
|
@@ -461,8 +499,7 @@ module Belt
|
|
|
461
499
|
Belt::CLI::TablesCommand.sync_all_environments
|
|
462
500
|
end
|
|
463
501
|
|
|
464
|
-
def
|
|
465
|
-
app_jsx = 'frontend/src/App.jsx'
|
|
502
|
+
def remove_view_routes_from(app_jsx)
|
|
466
503
|
return unless File.exist?(app_jsx)
|
|
467
504
|
|
|
468
505
|
content = File.read(app_jsx)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'open3'
|
|
5
|
+
require_relative '../../belt/inflector'
|
|
5
6
|
|
|
6
7
|
module Belt
|
|
7
8
|
module CLI
|
|
@@ -17,13 +18,14 @@ module Belt
|
|
|
17
18
|
|
|
18
19
|
def self.run(args)
|
|
19
20
|
verbose = args.include?('--verbose') || args.include?('-v')
|
|
21
|
+
preflight = args.include?('--preflight')
|
|
20
22
|
|
|
21
23
|
if args.include?('--help') || args.include?('-h')
|
|
22
24
|
puts usage
|
|
23
25
|
exit 0
|
|
24
26
|
end
|
|
25
27
|
|
|
26
|
-
new(verbose: verbose).run
|
|
28
|
+
new(verbose: verbose, preflight: preflight).run
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
def self.usage
|
|
@@ -33,24 +35,50 @@ module Belt
|
|
|
33
35
|
Check system dependencies and AWS configuration for Belt.
|
|
34
36
|
|
|
35
37
|
Options:
|
|
36
|
-
-v, --verbose
|
|
37
|
-
-
|
|
38
|
+
-v, --verbose Show detailed output for each check
|
|
39
|
+
--preflight Run only deploy-critical checks (indexes, credentials)
|
|
40
|
+
-h, --help Show this help
|
|
38
41
|
USAGE
|
|
39
42
|
end
|
|
40
43
|
|
|
41
|
-
def initialize(verbose: false)
|
|
44
|
+
def initialize(verbose: false, preflight: false)
|
|
42
45
|
@verbose = verbose
|
|
46
|
+
@preflight = preflight
|
|
43
47
|
@issues = []
|
|
44
48
|
@warnings = []
|
|
45
49
|
end
|
|
46
50
|
|
|
47
|
-
def run
|
|
51
|
+
def run # rubocop:disable Naming/PredicateMethod
|
|
52
|
+
if @preflight
|
|
53
|
+
run_preflight
|
|
54
|
+
else
|
|
55
|
+
run_full
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
@issues.empty?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Returns true if all preflight checks pass, false otherwise.
|
|
62
|
+
def run_preflight
|
|
63
|
+
check_aws_identity_quiet
|
|
64
|
+
check_table_indexes
|
|
65
|
+
check_cognito_auth
|
|
66
|
+
return if @issues.empty?
|
|
67
|
+
|
|
68
|
+
puts "\nPreflight checks failed:\n"
|
|
69
|
+
@issues.each { |i| puts " ✗ #{i}" }
|
|
70
|
+
puts ''
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def run_full
|
|
48
74
|
puts "Belt Doctor\n"
|
|
49
75
|
puts "Checking your system for Belt prerequisites...\n\n"
|
|
50
76
|
|
|
51
77
|
check_tools
|
|
52
78
|
check_aws_credentials
|
|
53
79
|
check_aws_identity
|
|
80
|
+
check_table_indexes
|
|
81
|
+
check_cognito_auth
|
|
54
82
|
|
|
55
83
|
puts ''
|
|
56
84
|
print_summary
|
|
@@ -132,28 +160,144 @@ module Belt
|
|
|
132
160
|
print_ok('Authentication', "account #{account}")
|
|
133
161
|
puts " ARN: #{arn}" if @verbose
|
|
134
162
|
else
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
163
|
+
handle_aws_identity_error(output.strip)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def check_aws_identity_quiet
|
|
168
|
+
_, status = Open3.capture2e('aws', 'sts', 'get-caller-identity')
|
|
169
|
+
return if status.success?
|
|
170
|
+
|
|
171
|
+
@issues << 'AWS credentials invalid or expired — run `aws sso login`'
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def handle_aws_identity_error(error)
|
|
175
|
+
if error.include?('ExpiredToken') || error.include?('expired')
|
|
176
|
+
print_fail('Authentication', 'credentials expired')
|
|
177
|
+
puts ' Run: aws sso login'
|
|
178
|
+
@issues << 'AWS credentials are expired — run `aws sso login`'
|
|
179
|
+
elsif error.include?('InvalidClientTokenId') || error.include?('SignatureDoesNotMatch')
|
|
180
|
+
print_fail('Authentication', 'invalid credentials')
|
|
181
|
+
puts ' Your access key or secret is incorrect.'
|
|
182
|
+
puts ' Run: aws configure'
|
|
183
|
+
@issues << 'AWS credentials are invalid'
|
|
184
|
+
elsif error.include?('Could not connect') || error.include?('Unable to locate credentials')
|
|
185
|
+
print_fail('Authentication', 'unable to authenticate')
|
|
186
|
+
puts ' Run: aws configure sso # or set AWS_PROFILE'
|
|
187
|
+
@issues << 'Unable to authenticate with AWS'
|
|
188
|
+
else
|
|
189
|
+
print_fail('Authentication', 'failed')
|
|
190
|
+
puts " #{error.lines.first&.strip}" if error.length.positive?
|
|
191
|
+
@issues << 'AWS authentication failed'
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def check_table_indexes
|
|
196
|
+
return unless Belt.root?
|
|
197
|
+
|
|
198
|
+
models_dir = File.join(Belt.root, 'lambda/models')
|
|
199
|
+
dynamodb_tf = File.join(Belt.root, 'infrastructure/modules/app/dynamodb.tf')
|
|
200
|
+
|
|
201
|
+
return unless Dir.exist?(models_dir)
|
|
202
|
+
|
|
203
|
+
puts ''
|
|
204
|
+
puts '── Tables & Indexes ──'
|
|
205
|
+
|
|
206
|
+
unless File.exist?(dynamodb_tf)
|
|
207
|
+
print_warn('dynamodb.tf', 'not found — run `belt setup tables` to generate')
|
|
208
|
+
@warnings << 'dynamodb.tf not generated — run `belt setup tables`'
|
|
209
|
+
return
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
tf_content = File.read(dynamodb_tf)
|
|
213
|
+
model_files = Dir.glob(File.join(models_dir, '*.rb'))
|
|
214
|
+
.reject { |f| File.basename(f) == 'application_record.rb' }
|
|
215
|
+
|
|
216
|
+
model_files.each do |file|
|
|
217
|
+
check_model_indexes(file, tf_content)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def check_model_indexes(file, tf_content)
|
|
222
|
+
content = File.read(file)
|
|
223
|
+
class_match = content.match(/^class\s+(\w+)\s*<\s*ApplicationRecord/)
|
|
224
|
+
return unless class_match
|
|
225
|
+
|
|
226
|
+
model_name = Belt::Inflector.underscore(class_match[1])
|
|
227
|
+
table_name = Belt::Inflector.pluralize(model_name)
|
|
228
|
+
|
|
229
|
+
# Check if table exists in dynamodb.tf
|
|
230
|
+
unless tf_content.include?("resource \"aws_dynamodb_table\" \"#{table_name}\"")
|
|
231
|
+
print_fail(table_name, 'table not in dynamodb.tf')
|
|
232
|
+
@issues << "Table '#{table_name}' missing from dynamodb.tf — run `belt setup tables`"
|
|
233
|
+
return
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Extract expected indexes from belongs_to
|
|
237
|
+
expected_indexes = extract_expected_indexes(content)
|
|
238
|
+
return if expected_indexes.empty?
|
|
239
|
+
|
|
240
|
+
# Check each expected index exists in the TF file
|
|
241
|
+
# Scope check to this table's resource block
|
|
242
|
+
table_block = tf_content[/resource "aws_dynamodb_table" "#{table_name}" \{.*?^\}/m]
|
|
243
|
+
return unless table_block
|
|
244
|
+
|
|
245
|
+
missing = []
|
|
246
|
+
expected_indexes.each do |idx|
|
|
247
|
+
if table_block.include?("name = \"#{idx[:name]}\"")
|
|
248
|
+
print_ok(table_name, idx[:name]) if @verbose
|
|
149
249
|
else
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
250
|
+
missing << idx
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
if missing.empty?
|
|
255
|
+
print_ok(table_name, "#{expected_indexes.size} index#{'es' if expected_indexes.size > 1} configured")
|
|
256
|
+
else
|
|
257
|
+
missing.each do |idx|
|
|
258
|
+
print_fail(table_name, "missing #{idx[:name]} (required by belongs_to :#{idx[:association]})")
|
|
259
|
+
@issues << "Table '#{table_name}' missing index '#{idx[:name]}' " \
|
|
260
|
+
'— run `belt setup tables` then `belt deploy`'
|
|
153
261
|
end
|
|
154
262
|
end
|
|
155
263
|
end
|
|
156
264
|
|
|
265
|
+
def extract_expected_indexes(content)
|
|
266
|
+
indexes = []
|
|
267
|
+
content.lines.reject { |line| line.strip.start_with?('#') }.join
|
|
268
|
+
.scan(/belongs_to\s+:(\w+)/) do |match|
|
|
269
|
+
association_name = match[0]
|
|
270
|
+
indexes << {
|
|
271
|
+
name: "#{Belt::Inflector.classify(association_name)}Index",
|
|
272
|
+
association: association_name
|
|
273
|
+
}
|
|
274
|
+
end
|
|
275
|
+
indexes
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def check_cognito_auth
|
|
279
|
+
return unless Belt.root?
|
|
280
|
+
|
|
281
|
+
routes_file = Belt.routes_file
|
|
282
|
+
return unless routes_file
|
|
283
|
+
|
|
284
|
+
content = File.read(routes_file)
|
|
285
|
+
|
|
286
|
+
# Check if any routes use auth: :cognito
|
|
287
|
+
uses_cognito = content.include?('auth: :cognito')
|
|
288
|
+
return unless uses_cognito
|
|
289
|
+
|
|
290
|
+
# Check if cognito.tf exists
|
|
291
|
+
cognito_tf = File.join(Belt.root, 'infrastructure/modules/app/cognito.tf')
|
|
292
|
+
return if File.exist?(cognito_tf)
|
|
293
|
+
|
|
294
|
+
puts ''
|
|
295
|
+
puts '── Authentication ──'
|
|
296
|
+
print_warn('Cognito', 'routes use auth: :cognito but no cognito.tf found')
|
|
297
|
+
puts ' Run: belt generate auth'
|
|
298
|
+
@warnings << 'Routes use auth: :cognito but no Cognito pool is configured — run `belt generate auth`'
|
|
299
|
+
end
|
|
300
|
+
|
|
157
301
|
def print_summary
|
|
158
302
|
if @issues.empty? && @warnings.empty?
|
|
159
303
|
puts '✓ All checks passed — you\'re ready to belt!'
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
require 'erb'
|
|
5
5
|
require_relative 'app_detection'
|
|
6
|
+
require_relative 'frontend_registry'
|
|
7
|
+
require_relative 'frontend_setup_command'
|
|
6
8
|
|
|
7
9
|
module Belt
|
|
8
10
|
module CLI
|
|
@@ -52,6 +54,8 @@ module Belt
|
|
|
52
54
|
puts " create #{dest_path}" unless @quiet
|
|
53
55
|
end
|
|
54
56
|
|
|
57
|
+
append_extra_frontend_outputs(File.join(dest_dir, 'outputs.tf'))
|
|
58
|
+
|
|
55
59
|
return if @quiet || !@announce
|
|
56
60
|
|
|
57
61
|
puts "\n✓ Environment '#{@env_name}' created!"
|
|
@@ -82,6 +86,14 @@ module Belt
|
|
|
82
86
|
File.write(dest_path, content)
|
|
83
87
|
end
|
|
84
88
|
|
|
89
|
+
def append_extra_frontend_outputs(outputs_file)
|
|
90
|
+
FrontendRegistry.new.all.each do |frontend|
|
|
91
|
+
next if frontend.tf_name == 'frontend'
|
|
92
|
+
|
|
93
|
+
FrontendSetupCommand.append_env_outputs_for(frontend, outputs_file)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
85
97
|
# Resolve the state bucket name to use in backend.tf.
|
|
86
98
|
# Priority: existing sibling backend.tf → AWS account ID → bare placeholder.
|
|
87
99
|
def resolve_state_bucket
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Belt
|
|
4
|
+
module CLI
|
|
5
|
+
class ExplainCommand
|
|
6
|
+
DOCS_DIR = File.expand_path('../docs', __dir__)
|
|
7
|
+
|
|
8
|
+
TOPICS = Dir.glob(File.join(DOCS_DIR, '*.md')).map do |path|
|
|
9
|
+
File.basename(path, '.md')
|
|
10
|
+
end.sort.freeze
|
|
11
|
+
|
|
12
|
+
ALIASES = {
|
|
13
|
+
'routes' => 'routing',
|
|
14
|
+
'route' => 'routing',
|
|
15
|
+
'router' => 'routing',
|
|
16
|
+
'controller' => 'controllers',
|
|
17
|
+
'model' => 'models',
|
|
18
|
+
'activeitem' => 'models',
|
|
19
|
+
'dynamodb' => 'models',
|
|
20
|
+
'deploy' => 'deployment',
|
|
21
|
+
'deploying' => 'deployment',
|
|
22
|
+
'terraform' => 'deployment',
|
|
23
|
+
'generate' => 'generators',
|
|
24
|
+
'generator' => 'generators',
|
|
25
|
+
'scaffold' => 'generators',
|
|
26
|
+
'handler' => 'lambda_handler',
|
|
27
|
+
'lambda' => 'lambda_handler',
|
|
28
|
+
'entry_point' => 'lambda_handler',
|
|
29
|
+
'entrypoint' => 'lambda_handler',
|
|
30
|
+
'project' => 'structure',
|
|
31
|
+
'layout' => 'structure',
|
|
32
|
+
'directory' => 'structure',
|
|
33
|
+
'logs' => 'observability',
|
|
34
|
+
'logging' => 'observability',
|
|
35
|
+
'metrics' => 'observability',
|
|
36
|
+
'backup' => 'backups',
|
|
37
|
+
'plugin' => 'plugins',
|
|
38
|
+
'irb' => 'console',
|
|
39
|
+
'repl' => 'console',
|
|
40
|
+
'frontends' => 'frontend',
|
|
41
|
+
'spa' => 'frontend'
|
|
42
|
+
}.freeze
|
|
43
|
+
|
|
44
|
+
def self.run(args)
|
|
45
|
+
if args.empty? || args.include?('--help') || args.include?('-h')
|
|
46
|
+
puts usage
|
|
47
|
+
return
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
topic = resolve_topic(args.first)
|
|
51
|
+
|
|
52
|
+
if topic.nil?
|
|
53
|
+
puts "Unknown topic: #{args.first}\n\n"
|
|
54
|
+
puts 'Available topics:'
|
|
55
|
+
TOPICS.each { |t| puts " #{t}" }
|
|
56
|
+
puts "\nRun `belt explain <topic>` for details."
|
|
57
|
+
exit 1
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
display_topic(topic)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.resolve_topic(input)
|
|
64
|
+
normalized = input.downcase.gsub('-', '_')
|
|
65
|
+
return normalized if TOPICS.include?(normalized)
|
|
66
|
+
return ALIASES[normalized] if ALIASES[normalized]
|
|
67
|
+
|
|
68
|
+
# Fuzzy match: find topics that start with or contain the input
|
|
69
|
+
match = TOPICS.find { |t| t.start_with?(normalized) }
|
|
70
|
+
match || TOPICS.find { |t| t.include?(normalized) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.display_topic(topic)
|
|
74
|
+
path = File.join(DOCS_DIR, "#{topic}.md")
|
|
75
|
+
content = File.read(path)
|
|
76
|
+
puts content
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.usage
|
|
80
|
+
topic_list = TOPICS.map { |t| " #{t}" }.join("\n")
|
|
81
|
+
|
|
82
|
+
<<~USAGE
|
|
83
|
+
Usage: belt explain <topic>
|
|
84
|
+
|
|
85
|
+
Display documentation for a Belt concept or feature.
|
|
86
|
+
|
|
87
|
+
Available topics:
|
|
88
|
+
#{topic_list}
|
|
89
|
+
|
|
90
|
+
Aliases:
|
|
91
|
+
routes, route, router → routing
|
|
92
|
+
controller → controllers
|
|
93
|
+
model, activeitem → models
|
|
94
|
+
deploy, terraform → deployment
|
|
95
|
+
generate, scaffold → generators
|
|
96
|
+
handler, lambda → lambda_handler
|
|
97
|
+
project, layout → structure
|
|
98
|
+
logs, logging, metrics → observability
|
|
99
|
+
backup → backups
|
|
100
|
+
plugin → plugins
|
|
101
|
+
irb, repl → console
|
|
102
|
+
|
|
103
|
+
Examples:
|
|
104
|
+
belt explain routing
|
|
105
|
+
belt explain controllers
|
|
106
|
+
belt explain deploy
|
|
107
|
+
belt explain models
|
|
108
|
+
USAGE
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -5,6 +5,7 @@ require 'erb'
|
|
|
5
5
|
require 'json'
|
|
6
6
|
require 'open3'
|
|
7
7
|
require_relative 'app_detection'
|
|
8
|
+
require_relative 'frontend_registry'
|
|
8
9
|
|
|
9
10
|
module Belt
|
|
10
11
|
module CLI
|
|
@@ -15,33 +16,39 @@ module Belt
|
|
|
15
16
|
include AppDetection
|
|
16
17
|
|
|
17
18
|
def self.run(args)
|
|
19
|
+
name = FrontendRegistry.extract_flag!(args, '--name')
|
|
20
|
+
path = FrontendRegistry.extract_flag!(args, '--path')
|
|
18
21
|
framework = args.shift
|
|
19
22
|
|
|
20
23
|
if framework.nil? || !FRAMEWORKS.include?(framework)
|
|
21
|
-
puts "Usage: belt generate frontend <#{FRAMEWORKS.join('|')}>"
|
|
24
|
+
puts "Usage: belt generate frontend <#{FRAMEWORKS.join('|')}> [--name NAME] [--path DIR]"
|
|
22
25
|
puts "\nScaffolds a frontend application with build tooling and API client."
|
|
23
26
|
puts "\nExamples:"
|
|
24
27
|
puts ' belt generate frontend react'
|
|
25
28
|
puts ' belt generate frontend vue'
|
|
29
|
+
puts ' belt generate frontend react --name ops --path ops-app'
|
|
26
30
|
exit 1
|
|
27
31
|
end
|
|
28
32
|
|
|
29
|
-
new(framework).generate
|
|
33
|
+
new(framework, name: name, path: path).generate
|
|
30
34
|
end
|
|
31
35
|
|
|
32
|
-
def initialize(framework, quiet: false, announce: true)
|
|
36
|
+
def initialize(framework, quiet: false, announce: true, name: nil, path: nil)
|
|
33
37
|
@framework = framework
|
|
34
38
|
@quiet = quiet
|
|
35
39
|
@announce = announce
|
|
36
40
|
@app_name = detect_app_name
|
|
37
41
|
@module_name = @app_name.split(/[-_]/).map(&:capitalize).join
|
|
42
|
+
@name = name || (path ? File.basename(path) : 'frontend')
|
|
43
|
+
@path = path || name || 'frontend'
|
|
44
|
+
@frontend = Frontend.new(name: @name, path: @path, default: @name == 'frontend' && @path == 'frontend')
|
|
38
45
|
end
|
|
39
46
|
|
|
40
47
|
def generate
|
|
41
|
-
dest_dir =
|
|
48
|
+
dest_dir = @frontend.path
|
|
42
49
|
|
|
43
50
|
if Dir.exist?(dest_dir) && !Dir.empty?(dest_dir)
|
|
44
|
-
puts "Directory '
|
|
51
|
+
puts "Directory '#{dest_dir}/' already exists and is not empty."
|
|
45
52
|
exit 1
|
|
46
53
|
end
|
|
47
54
|
|
|
@@ -54,8 +61,9 @@ module Belt
|
|
|
54
61
|
end
|
|
55
62
|
|
|
56
63
|
copy_template(framework_dir, dest_dir)
|
|
64
|
+
register_frontend!
|
|
57
65
|
|
|
58
|
-
puts "\n✓ Frontend (#{@framework}) created in
|
|
66
|
+
puts "\n✓ Frontend (#{@framework}) created in #{dest_dir}/" unless @quiet
|
|
59
67
|
|
|
60
68
|
install_dependencies(dest_dir)
|
|
61
69
|
setup_frontend_infra_for_existing_environments
|
|
@@ -64,7 +72,11 @@ module Belt
|
|
|
64
72
|
return if @quiet || !@announce
|
|
65
73
|
|
|
66
74
|
puts "\nNext steps:"
|
|
67
|
-
|
|
75
|
+
if @frontend.name == 'frontend'
|
|
76
|
+
puts ' belt server # Start local dev server'
|
|
77
|
+
else
|
|
78
|
+
puts " belt server --frontend #{@frontend.name}"
|
|
79
|
+
end
|
|
68
80
|
puts ' belt deploy # Deploy everything to AWS'
|
|
69
81
|
end
|
|
70
82
|
|
|
@@ -88,9 +100,18 @@ module Belt
|
|
|
88
100
|
end
|
|
89
101
|
end
|
|
90
102
|
|
|
103
|
+
def register_frontend!
|
|
104
|
+
custom = @frontend.name != 'frontend' || @frontend.path != 'frontend'
|
|
105
|
+
config_exists = FrontendRegistry.find_config_path
|
|
106
|
+
return unless custom || config_exists
|
|
107
|
+
|
|
108
|
+
dest = FrontendRegistry.register!(name: @frontend.name, path: @frontend.path)
|
|
109
|
+
puts " update #{dest} (#{@frontend.name})" unless @quiet
|
|
110
|
+
end
|
|
111
|
+
|
|
91
112
|
def setup_frontend_infra_for_existing_environments
|
|
92
113
|
module_dir = 'infrastructure/modules/app'
|
|
93
|
-
frontend_tf = File.join(module_dir, 'frontend.tf
|
|
114
|
+
frontend_tf = File.join(module_dir, "#{@frontend.tf_name == 'frontend' ? 'frontend' : @frontend.tf_name}.tf")
|
|
94
115
|
|
|
95
116
|
if File.exist?(frontend_tf)
|
|
96
117
|
puts " skip #{frontend_tf} (already exists)" unless @quiet
|
|
@@ -101,7 +122,7 @@ module Belt
|
|
|
101
122
|
|
|
102
123
|
puts "\n Setting up frontend infrastructure..." unless @quiet
|
|
103
124
|
require_relative 'frontend_setup_command'
|
|
104
|
-
FrontendSetupCommand.new(nil, quiet: true).run
|
|
125
|
+
FrontendSetupCommand.new(nil, quiet: true, frontend: @frontend).run
|
|
105
126
|
puts " create #{frontend_tf}" unless @quiet
|
|
106
127
|
end
|
|
107
128
|
|
|
@@ -118,7 +139,7 @@ module Belt
|
|
|
118
139
|
require_relative 'views_command'
|
|
119
140
|
resources.each do |resource_name|
|
|
120
141
|
fields = ViewsCommand.read_schema_fields(resource_name)
|
|
121
|
-
ViewsCommand.new(resource_name, fields, force: true).generate
|
|
142
|
+
ViewsCommand.new(resource_name, fields, force: true, frontend: @frontend).generate
|
|
122
143
|
end
|
|
123
144
|
end
|
|
124
145
|
|