belt 0.1.10 → 0.1.12
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 +10 -0
- data/lib/belt/action_router.rb +1 -4
- data/lib/belt/cli/deploy_command.rb +25 -13
- data/lib/belt/cli/destroy_command.rb +17 -17
- data/lib/belt/cli/environment_command.rb +9 -9
- data/lib/belt/cli/frontend_command.rb +1 -1
- data/lib/belt/cli/frontend_deploy_command.rb +1 -3
- data/lib/belt/cli/frontend_env_map.rb +2 -2
- data/lib/belt/cli/frontend_setup_command.rb +3 -3
- data/lib/belt/cli/generate_command.rb +11 -8
- data/lib/belt/cli/new_command.rb +8 -4
- data/lib/belt/cli/routes_command/route_inference.rb +3 -1
- data/lib/belt/cli/server_command.rb +6 -12
- data/lib/belt/cli/setup_command.rb +8 -10
- data/lib/belt/cli/tables_command.rb +4 -4
- data/lib/belt/cli.rb +1 -4
- data/lib/belt/controllers/welcome_controller.rb +2 -2
- data/lib/belt/lambda_handler.rb +7 -7
- data/lib/belt/route_dsl.rb +93 -8
- data/lib/belt/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 892f04b0175a10793ba63f3cf65182b3d3e380ab75446984004283b8b8cf7377
|
|
4
|
+
data.tar.gz: 701534fd954f5d444783d3adb085d0582c4f9d1b92bf4518249e677add64c1b7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '0682c1d964357b0109baca143e7c43fb8688a375aa84af0ba3ba2c46fbf0d56932a011b8eef6a0027dcf6b289f3eb6bc4617bcecd8717812246133b107563233'
|
|
7
|
+
data.tar.gz: 68d1e8b3a4311ca6bd3be6dbfeb48594efeb4afaf42500d27b7076e0068a8b21c8cd73a1e41a70c4ee6338e910dab96aaa8fdba09683c922027574141049a899
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.11
|
|
6
|
+
|
|
7
|
+
### Route DSL
|
|
8
|
+
|
|
9
|
+
- `resources` / `resource` honor `scope path:` (and nested path scopes).
|
|
10
|
+
Example: `scope path: "admin", auth: :cognito { resources :users }` → `/admin/users…` with Cognito auth.
|
|
11
|
+
- Nested `scope path:` segments stack (`a` then `b` → `a/b`) instead of replacing.
|
|
12
|
+
- `scope controller:` is applied to `resources` / `resource` the same way as verb routes.
|
|
13
|
+
- Controller inference for multi-segment resource paths joins segments (`/admin/users` → `admin/users`), matching nested-resource convention.
|
|
14
|
+
|
|
5
15
|
## 0.1.10
|
|
6
16
|
|
|
7
17
|
### Frontend env map
|
data/lib/belt/action_router.rb
CHANGED
|
@@ -157,10 +157,7 @@ module Belt
|
|
|
157
157
|
|
|
158
158
|
# Final fallback: gem-embedded controllers under Belt:: namespace
|
|
159
159
|
class_name = "#{controller_name.split(%r{[_/]}).map(&:capitalize).join}Controller"
|
|
160
|
-
|
|
161
|
-
if Belt.const_defined?(class_name, false)
|
|
162
|
-
return Belt.const_get(class_name, false)
|
|
163
|
-
end
|
|
160
|
+
return Belt.const_get(class_name, false) if Belt.const_defined?(class_name, false)
|
|
164
161
|
|
|
165
162
|
raise Belt::ActionNotFound, "Controller not found: #{controller_name}"
|
|
166
163
|
end
|
|
@@ -121,7 +121,8 @@ module Belt
|
|
|
121
121
|
Dir.chdir(env_dir) do
|
|
122
122
|
run_init
|
|
123
123
|
run_plan
|
|
124
|
-
return unless confirm_apply
|
|
124
|
+
return unless confirm_apply?
|
|
125
|
+
|
|
125
126
|
run_apply
|
|
126
127
|
end
|
|
127
128
|
|
|
@@ -157,6 +158,7 @@ module Belt
|
|
|
157
158
|
dir = @infra_dir ? File.dirname(@infra_dir) : Dir.pwd
|
|
158
159
|
while dir != '/'
|
|
159
160
|
return dir if File.exist?(File.join(dir, 'Gemfile'))
|
|
161
|
+
|
|
160
162
|
dir = File.dirname(dir)
|
|
161
163
|
end
|
|
162
164
|
Dir.pwd
|
|
@@ -188,9 +190,13 @@ module Belt
|
|
|
188
190
|
stdout, status = Open3.capture2('aws', 'sts', 'get-caller-identity', '--output', 'json')
|
|
189
191
|
unless status.success?
|
|
190
192
|
abort "Error: AWS credentials not available.\n" \
|
|
191
|
-
|
|
193
|
+
'Run `aws sso login` or configure AWS_PROFILE.'
|
|
194
|
+
end
|
|
195
|
+
@aws_account = begin
|
|
196
|
+
JSON.parse(stdout)['Account']
|
|
197
|
+
rescue StandardError
|
|
198
|
+
nil
|
|
192
199
|
end
|
|
193
|
-
@aws_account = JSON.parse(stdout)['Account'] rescue nil
|
|
194
200
|
end
|
|
195
201
|
|
|
196
202
|
# Ensure Gemfile.lock doesn't have stale PATH references that will break
|
|
@@ -208,7 +214,8 @@ module Belt
|
|
|
208
214
|
stale_path_gems = detect_stale_path_gems(gemfile_content, lockfile_content)
|
|
209
215
|
return if stale_path_gems.empty?
|
|
210
216
|
|
|
211
|
-
puts
|
|
217
|
+
puts ' 🔧 Fixing stale Gemfile.lock ' \
|
|
218
|
+
"(#{stale_path_gems.join(', ')} referenced as PATH but Gemfile uses RubyGems)"
|
|
212
219
|
Dir.chdir(@project_root) do
|
|
213
220
|
success = system('bundle', 'lock', '--update', *stale_path_gems)
|
|
214
221
|
unless success
|
|
@@ -250,7 +257,11 @@ module Belt
|
|
|
250
257
|
output, status = Open3.capture2('terraform', 'output', '-json')
|
|
251
258
|
return nil unless status.success?
|
|
252
259
|
|
|
253
|
-
data =
|
|
260
|
+
data = begin
|
|
261
|
+
JSON.parse(output)
|
|
262
|
+
rescue StandardError
|
|
263
|
+
{}
|
|
264
|
+
end
|
|
254
265
|
|
|
255
266
|
# Try lambda_functions output first
|
|
256
267
|
if data['lambda_functions']
|
|
@@ -372,9 +383,10 @@ module Belt
|
|
|
372
383
|
end
|
|
373
384
|
|
|
374
385
|
# Remove non-essential files
|
|
375
|
-
exts = %w[*.md *.rdoc *.txt *.c *.h *.o Makefile *.log CHANGELOG* HISTORY* LICENSE* README* .gitignore
|
|
386
|
+
exts = %w[*.md *.rdoc *.txt *.c *.h *.o Makefile *.log CHANGELOG* HISTORY* LICENSE* README* .gitignore
|
|
387
|
+
.travis.yml .rubocop.yml Rakefile]
|
|
376
388
|
exts.each do |pattern|
|
|
377
|
-
Dir.glob(File.join(vendor_dir,
|
|
389
|
+
Dir.glob(File.join(vendor_dir, '**', pattern)).each do |f|
|
|
378
390
|
File.delete(f) if File.file?(f)
|
|
379
391
|
end
|
|
380
392
|
end
|
|
@@ -409,12 +421,12 @@ module Belt
|
|
|
409
421
|
'--output', 'json'
|
|
410
422
|
)
|
|
411
423
|
|
|
412
|
-
|
|
424
|
+
FileUtils.rm_f(zip_file)
|
|
413
425
|
|
|
414
426
|
unless status.success?
|
|
415
|
-
abort "\n✗ Failed to update Lambda function code.\n" \
|
|
416
|
-
"
|
|
417
|
-
|
|
427
|
+
abort "\n✗ Failed to update Lambda function code.\n " \
|
|
428
|
+
"Function: #{function_name}\n " \
|
|
429
|
+
'Check that the function exists and you have permissions.'
|
|
418
430
|
end
|
|
419
431
|
|
|
420
432
|
# Wait for update to complete
|
|
@@ -454,7 +466,7 @@ module Belt
|
|
|
454
466
|
puts ''
|
|
455
467
|
end
|
|
456
468
|
|
|
457
|
-
def confirm_apply
|
|
469
|
+
def confirm_apply?
|
|
458
470
|
return true if @auto_approve
|
|
459
471
|
|
|
460
472
|
if @env == 'prod' || @env == 'production'
|
|
@@ -479,7 +491,7 @@ module Belt
|
|
|
479
491
|
end
|
|
480
492
|
|
|
481
493
|
def cleanup_plan
|
|
482
|
-
|
|
494
|
+
FileUtils.rm_f('tfplan')
|
|
483
495
|
end
|
|
484
496
|
|
|
485
497
|
def deploy_frontend_if_exists
|
|
@@ -237,11 +237,11 @@ module Belt
|
|
|
237
237
|
# Clean up trailing commas and empty arrays
|
|
238
238
|
content.gsub!(/,(\s*\n\s*\]\.freeze)/, '\1')
|
|
239
239
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
240
|
+
return unless content != original
|
|
241
|
+
|
|
242
|
+
File.write(manifest_file, content)
|
|
243
|
+
@updated << manifest_file
|
|
244
|
+
puts " update #{manifest_file}"
|
|
245
245
|
end
|
|
246
246
|
|
|
247
247
|
def remove_schema
|
|
@@ -257,11 +257,11 @@ module Belt
|
|
|
257
257
|
# Clean up excessive blank lines
|
|
258
258
|
content.gsub!(/\n{3,}/, "\n\n")
|
|
259
259
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
260
|
+
return unless content != original
|
|
261
|
+
|
|
262
|
+
File.write(schema_file, content)
|
|
263
|
+
@updated << schema_file
|
|
264
|
+
puts " update #{schema_file}"
|
|
265
265
|
end
|
|
266
266
|
|
|
267
267
|
def sync_tables
|
|
@@ -276,16 +276,16 @@ module Belt
|
|
|
276
276
|
original = content.dup
|
|
277
277
|
|
|
278
278
|
# Remove import lines for this resource's pages
|
|
279
|
-
content.gsub!(
|
|
279
|
+
content.gsub!(%r{^import #{@class_name}\w* from './pages/#{@resource_name}/.*'\n}, '')
|
|
280
280
|
|
|
281
281
|
# Remove Route elements for this resource
|
|
282
|
-
content.gsub!(
|
|
282
|
+
content.gsub!(%r{^\s*<Route path="/#{@resource_name}.*?/>\n}, '')
|
|
283
283
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
284
|
+
return unless content != original
|
|
285
|
+
|
|
286
|
+
File.write(app_jsx, content)
|
|
287
|
+
@updated << app_jsx
|
|
288
|
+
puts " update #{app_jsx}"
|
|
289
289
|
end
|
|
290
290
|
|
|
291
291
|
def print_summary
|
|
@@ -52,15 +52,15 @@ module Belt
|
|
|
52
52
|
|
|
53
53
|
puts "\n✓ Environment '#{@env_name}' created!"
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
return if @quiet
|
|
56
|
+
|
|
57
|
+
puts "\nNext steps:"
|
|
58
|
+
puts " cd #{dest_dir}"
|
|
59
|
+
puts ' terraform init'
|
|
60
|
+
puts ' terraform plan'
|
|
61
|
+
puts ' terraform apply'
|
|
62
|
+
puts "\nTo configure a custom domain, set `domain` in #{dest_dir}/terraform.tfvars:"
|
|
63
|
+
puts ' domain = "myapp.com"'
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
private
|
|
@@ -68,7 +68,7 @@ module Belt
|
|
|
68
68
|
puts "\n Installing npm dependencies..."
|
|
69
69
|
success = system('npm', 'install', '--prefix', dest_dir, '--loglevel', 'error')
|
|
70
70
|
if success
|
|
71
|
-
puts
|
|
71
|
+
puts ' ✓ Dependencies installed'
|
|
72
72
|
else
|
|
73
73
|
puts " ⚠ npm install failed — run `cd #{dest_dir} && npm install` manually"
|
|
74
74
|
end
|
|
@@ -62,9 +62,7 @@ module Belt
|
|
|
62
62
|
|
|
63
63
|
puts '🏗️ Building frontend...'
|
|
64
64
|
env = frontend_build_env
|
|
65
|
-
if env.any?
|
|
66
|
-
puts " Injecting env: #{env.keys.sort.join(', ')}"
|
|
67
|
-
end
|
|
65
|
+
puts " Injecting env: #{env.keys.sort.join(', ')}" if env.any?
|
|
68
66
|
run!(env, 'npm', 'run', 'build', chdir: 'frontend')
|
|
69
67
|
end
|
|
70
68
|
|
|
@@ -112,7 +112,7 @@ module Belt
|
|
|
112
112
|
def load_map
|
|
113
113
|
return DEFAULT_MAP.dup if @map_path.nil?
|
|
114
114
|
|
|
115
|
-
raw = YAML.
|
|
115
|
+
raw = YAML.safe_load_file(@map_path, aliases: false)
|
|
116
116
|
unless raw.is_a?(Hash) && raw.any?
|
|
117
117
|
abort "Error: frontend env map #{@map_path} must be a non-empty YAML mapping " \
|
|
118
118
|
'(e.g. VITE_API_URL: api_url).'
|
|
@@ -124,7 +124,7 @@ module Belt
|
|
|
124
124
|
next if k.empty? || v.empty?
|
|
125
125
|
|
|
126
126
|
hash[k] = v
|
|
127
|
-
end.tap do |parsed|
|
|
127
|
+
end.tap do |parsed| # rubocop:disable Style/MultilineBlockChain
|
|
128
128
|
abort "Error: frontend env map #{@map_path} has no valid entries." if parsed.empty?
|
|
129
129
|
end
|
|
130
130
|
rescue Psych::SyntaxError => e
|
|
@@ -20,7 +20,7 @@ module Belt
|
|
|
20
20
|
new.run
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
def initialize(
|
|
23
|
+
def initialize(_env = nil, quiet: false)
|
|
24
24
|
@app_name = detect_app_name
|
|
25
25
|
@quiet = quiet
|
|
26
26
|
end
|
|
@@ -32,7 +32,7 @@ module Belt
|
|
|
32
32
|
|
|
33
33
|
puts "\n✓ Frontend infrastructure generated in #{MODULE_DIR}!"
|
|
34
34
|
puts "\nRun `belt deploy` to create the S3 bucket and CloudFront distribution."
|
|
35
|
-
puts
|
|
35
|
+
puts 'Then `belt deploy frontend` to build and deploy.'
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
private
|
|
@@ -41,7 +41,7 @@ module Belt
|
|
|
41
41
|
return if Dir.exist?(MODULE_DIR)
|
|
42
42
|
|
|
43
43
|
abort "Error: Module directory not found at #{MODULE_DIR}/.\n" \
|
|
44
|
-
|
|
44
|
+
'Run `belt new` to create a project with the correct structure.'
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def generate_frontend_tf
|
|
@@ -139,14 +139,15 @@ module Belt
|
|
|
139
139
|
errors << 'must only contain letters, numbers, and underscores' unless name.match?(/\A[a-zA-Z][a-zA-Z0-9_]*\z/)
|
|
140
140
|
errors << 'must not start or end with an underscore' if name.match?(/\A_|_\z/)
|
|
141
141
|
errors << 'must not contain consecutive underscores' if name.include?('__')
|
|
142
|
-
errors <<
|
|
143
|
-
errors <<
|
|
142
|
+
errors << 'is a reserved name' if RESERVED_NAMES.include?(Belt::Inflector.singularize(name.downcase))
|
|
143
|
+
errors << 'is a reserved name' if RESERVED_NAMES.include?(name.downcase)
|
|
144
144
|
|
|
145
145
|
return if errors.empty?
|
|
146
146
|
|
|
147
147
|
puts "\n✗ Invalid #{generator} name '#{name}':"
|
|
148
148
|
errors.uniq.each { |e| puts " - #{e}" }
|
|
149
|
-
puts "\nName must start with a letter, be at least 2 characters,
|
|
149
|
+
puts "\nName must start with a letter, be at least 2 characters, " \
|
|
150
|
+
'and contain only letters, numbers, and single underscores.'
|
|
150
151
|
exit 1
|
|
151
152
|
end
|
|
152
153
|
|
|
@@ -199,13 +200,13 @@ module Belt
|
|
|
199
200
|
if info[:options].any?
|
|
200
201
|
puts "\nOptions:"
|
|
201
202
|
info[:options].each do |flag, desc|
|
|
202
|
-
puts
|
|
203
|
+
puts format(' %<flag>-20s %<desc>s', flag: flag, desc: desc)
|
|
203
204
|
end
|
|
204
205
|
end
|
|
205
206
|
|
|
206
207
|
puts "\nField Types:"
|
|
207
208
|
puts " #{FIELD_TYPES.join(', ')}"
|
|
208
|
-
puts
|
|
209
|
+
puts ' (defaults to string if omitted)'
|
|
209
210
|
|
|
210
211
|
puts "\nExamples:"
|
|
211
212
|
info[:examples].each do |cmd, desc|
|
|
@@ -249,7 +250,9 @@ module Belt
|
|
|
249
250
|
def check_resource_collision!
|
|
250
251
|
conflicts = []
|
|
251
252
|
conflicts << "lambda/models/#{@singular_name}.rb" if File.exist?("lambda/models/#{@singular_name}.rb")
|
|
252
|
-
|
|
253
|
+
if File.exist?("lambda/controllers/#{@app_name}/#{@resource_name}_controller.rb")
|
|
254
|
+
conflicts << "lambda/controllers/#{@app_name}/#{@resource_name}_controller.rb"
|
|
255
|
+
end
|
|
253
256
|
|
|
254
257
|
schema_file = 'infrastructure/schema.tf.rb'
|
|
255
258
|
if File.exist?(schema_file) && File.read(schema_file).match?(/^\s*model :#{Regexp.escape(@singular_name)}\b/)
|
|
@@ -348,7 +351,7 @@ module Belt
|
|
|
348
351
|
|
|
349
352
|
if content.match?(namespace_pattern)
|
|
350
353
|
content.sub!(namespace_pattern) do |match|
|
|
351
|
-
indent =
|
|
354
|
+
indent = ::Regexp.last_match(1)
|
|
352
355
|
# Insert the resource line before the namespace's closing `end`
|
|
353
356
|
match.sub(/^(#{indent})end\z/m, "#{indent} #{resource_line}\n#{indent}end")
|
|
354
357
|
end
|
|
@@ -357,7 +360,7 @@ module Belt
|
|
|
357
360
|
single_ns_pattern = /^(\s*)namespace :\w+\b[^\n]*do\s*\n(.*?)^\1end/m
|
|
358
361
|
if content.match?(single_ns_pattern)
|
|
359
362
|
content.sub!(single_ns_pattern) do |match|
|
|
360
|
-
indent =
|
|
363
|
+
indent = ::Regexp.last_match(1)
|
|
361
364
|
match.sub(/^(#{indent})end\z/m, "#{indent} #{resource_line}\n#{indent}end")
|
|
362
365
|
end
|
|
363
366
|
else
|
data/lib/belt/cli/new_command.rb
CHANGED
|
@@ -185,7 +185,7 @@ module Belt
|
|
|
185
185
|
if aws_configured?
|
|
186
186
|
puts "\n Setting up Terraform state bucket..."
|
|
187
187
|
begin
|
|
188
|
-
Belt::CLI::SetupCommand.new([
|
|
188
|
+
Belt::CLI::SetupCommand.new(['--bucket', @resolved_bucket]).run_state_setup
|
|
189
189
|
@state_setup_succeeded = true
|
|
190
190
|
rescue SystemExit
|
|
191
191
|
puts ' ⚠ State bucket setup encountered an issue — run `belt setup state` to retry.'
|
|
@@ -195,7 +195,7 @@ module Belt
|
|
|
195
195
|
puts "\n State bucket: #{@resolved_bucket}"
|
|
196
196
|
puts " State keys: #{s3_safe_name(@app_name)}/<env>/terraform.tfstate"
|
|
197
197
|
if @aws_error&.include?('ForbiddenException') || @aws_error&.include?('AccessDenied')
|
|
198
|
-
puts
|
|
198
|
+
puts ' ⚠ AWS credentials found but access denied — check your profile/role configuration.'
|
|
199
199
|
puts " #{@aws_error}" if @aws_error
|
|
200
200
|
else
|
|
201
201
|
puts ' ⚠ AWS credentials not detected — skipping state bucket creation.'
|
|
@@ -209,7 +209,11 @@ module Belt
|
|
|
209
209
|
def aws_configured?
|
|
210
210
|
output, status = Open3.capture2e('aws', 'sts', 'get-caller-identity')
|
|
211
211
|
if status.success?
|
|
212
|
-
data =
|
|
212
|
+
data = begin
|
|
213
|
+
JSON.parse(output)
|
|
214
|
+
rescue StandardError
|
|
215
|
+
{}
|
|
216
|
+
end
|
|
213
217
|
@aws_account_id = data['Account']
|
|
214
218
|
true
|
|
215
219
|
else
|
|
@@ -284,7 +288,7 @@ module Belt
|
|
|
284
288
|
puts ' → Edit → paste the 4 values from step 1.'
|
|
285
289
|
puts ''
|
|
286
290
|
puts ' 3. Wait for propagation (usually 5–30 min, can take up to 48h).'
|
|
287
|
-
puts
|
|
291
|
+
puts " Verify: dig +short NS #{@domain}"
|
|
288
292
|
puts ' ─────────────────────────────────────────────────────────────────'
|
|
289
293
|
else
|
|
290
294
|
puts "\n To add a custom domain later, set `domain` in infrastructure/<env>/terraform.tfvars:"
|
|
@@ -14,7 +14,9 @@ module Belt
|
|
|
14
14
|
non_param = segments.reject { |s| s.start_with?(':', '{') }
|
|
15
15
|
return gateway.name if non_param.empty?
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
# Nested resources (/posts/{id}/comments) and scoped resources (/admin/users):
|
|
18
|
+
# join non-param segments → posts/comments, admin/users
|
|
19
|
+
return non_param.map { |s| s.gsub('-', '_') }.join('/') if route.resource? && non_param.length > 1
|
|
18
20
|
|
|
19
21
|
# For non-resource routes with a single segment (e.g., post '/signup' in :onboarding),
|
|
20
22
|
# the segment is the action name, not the controller. Use the gateway name as controller.
|
|
@@ -94,9 +94,7 @@ module Belt
|
|
|
94
94
|
else
|
|
95
95
|
puts ' Backend is serverless — deploy with `belt deploy` to set up AWS resources.'
|
|
96
96
|
end
|
|
97
|
-
if build_env.any?
|
|
98
|
-
puts " Env: #{build_env.keys.sort.join(', ')}"
|
|
99
|
-
end
|
|
97
|
+
puts " Env: #{build_env.keys.sort.join(', ')}" if build_env.any?
|
|
100
98
|
puts ''
|
|
101
99
|
|
|
102
100
|
open_browser_later if @open_browser
|
|
@@ -121,24 +119,20 @@ module Belt
|
|
|
121
119
|
end
|
|
122
120
|
|
|
123
121
|
def run_welcome_server
|
|
122
|
+
puts "🚀 Serving Belt welcome page on http://localhost:#{@port}"
|
|
124
123
|
if @api_url
|
|
125
|
-
puts "🚀 Serving Belt welcome page on http://localhost:#{@port}"
|
|
126
124
|
puts " Backend API: #{@api_url}"
|
|
127
|
-
puts ''
|
|
128
|
-
puts ' Tip: Run `belt generate frontend react` to scaffold a frontend app.'
|
|
129
|
-
puts ''
|
|
130
125
|
else
|
|
131
|
-
puts "🚀 Serving Belt welcome page on http://localhost:#{@port}"
|
|
132
126
|
puts ' No deployment detected — showing pre-deploy welcome page.'
|
|
133
127
|
puts ' Backend is serverless — deploy with `belt deploy` to set up AWS resources.'
|
|
134
|
-
puts ''
|
|
135
|
-
puts ' Tip: Run `belt generate frontend react` to scaffold a frontend app.'
|
|
136
|
-
puts ''
|
|
137
128
|
end
|
|
129
|
+
puts ''
|
|
130
|
+
puts ' Tip: Run `belt generate frontend react` to scaffold a frontend app.'
|
|
131
|
+
puts ''
|
|
138
132
|
|
|
139
133
|
require 'webrick'
|
|
140
134
|
|
|
141
|
-
server = WEBrick::HTTPServer.new(Port: @port, Logger: WEBrick::Log.new(
|
|
135
|
+
server = WEBrick::HTTPServer.new(Port: @port, Logger: WEBrick::Log.new(File::NULL), AccessLog: [])
|
|
142
136
|
|
|
143
137
|
server.mount_proc '/' do |_req, res|
|
|
144
138
|
res['Content-Type'] = 'text/html; charset=utf-8'
|
|
@@ -53,7 +53,7 @@ module Belt
|
|
|
53
53
|
def run_state_setup
|
|
54
54
|
unless aws_configured?
|
|
55
55
|
if @aws_error&.include?('ForbiddenException') || @aws_error&.include?('AccessDenied')
|
|
56
|
-
puts
|
|
56
|
+
puts '✗ AWS credentials found but access denied — check your profile/role configuration.'
|
|
57
57
|
puts " #{@aws_error}"
|
|
58
58
|
else
|
|
59
59
|
puts '✗ AWS credentials not configured. Set AWS_PROFILE or configure aws sso login.'
|
|
@@ -139,11 +139,7 @@ module Belt
|
|
|
139
139
|
end
|
|
140
140
|
|
|
141
141
|
def resolve_bucket_name
|
|
142
|
-
|
|
143
|
-
@custom_bucket
|
|
144
|
-
else
|
|
145
|
-
'belt-terraform-state'
|
|
146
|
-
end
|
|
142
|
+
@custom_bucket || 'belt-terraform-state'
|
|
147
143
|
end
|
|
148
144
|
|
|
149
145
|
# --- Interactive selection ---
|
|
@@ -192,7 +188,11 @@ module Belt
|
|
|
192
188
|
def aws_configured?
|
|
193
189
|
output, status = Open3.capture2e('aws', 'sts', 'get-caller-identity')
|
|
194
190
|
if status.success?
|
|
195
|
-
data =
|
|
191
|
+
data = begin
|
|
192
|
+
JSON.parse(output)
|
|
193
|
+
rescue StandardError
|
|
194
|
+
{}
|
|
195
|
+
end
|
|
196
196
|
@aws_account_id = data['Account']
|
|
197
197
|
true
|
|
198
198
|
else
|
|
@@ -201,9 +201,7 @@ module Belt
|
|
|
201
201
|
end
|
|
202
202
|
end
|
|
203
203
|
|
|
204
|
-
|
|
205
|
-
@aws_account_id
|
|
206
|
-
end
|
|
204
|
+
attr_reader :aws_account_id
|
|
207
205
|
|
|
208
206
|
def bucket_exists?(bucket)
|
|
209
207
|
output, status = Open3.capture2e('aws', 's3api', 'head-bucket', '--bucket', bucket)
|
|
@@ -65,10 +65,10 @@ module Belt
|
|
|
65
65
|
end
|
|
66
66
|
return if Dir.exist?(MODULE_DIR)
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
return if @quiet
|
|
69
|
+
|
|
70
|
+
abort "Error: Module directory not found at #{MODULE_DIR}/.\n" \
|
|
71
|
+
'Run `belt new` to create a project with the correct structure.'
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
def parse_schema
|
data/lib/belt/cli.rb
CHANGED
|
@@ -35,7 +35,6 @@ module Belt
|
|
|
35
35
|
%w[version --version -v] => ->(_args) { puts "Belt #{Belt::VERSION}" }
|
|
36
36
|
}.freeze
|
|
37
37
|
|
|
38
|
-
|
|
39
38
|
COMMANDS = COMMANDS_DEFINITION.each_with_object({}) do |(keys, handler), hash|
|
|
40
39
|
Array(keys).each { |key| hash[key] = handler }
|
|
41
40
|
end.freeze
|
|
@@ -56,9 +55,7 @@ module Belt
|
|
|
56
55
|
if args.empty? || args.first =~ /\A-/
|
|
57
56
|
# belt destroy --help → DestroyCommand help
|
|
58
57
|
# belt destroy (no args) → terraform destroy (needs env)
|
|
59
|
-
if args.include?('--help') || args.include?('-h')
|
|
60
|
-
return DestroyCommand.run(args)
|
|
61
|
-
end
|
|
58
|
+
return DestroyCommand.run(args) if args.include?('--help') || args.include?('-h')
|
|
62
59
|
elsif DestroyCommand::GENERATORS.include?(args.first)
|
|
63
60
|
return DestroyCommand.run(args)
|
|
64
61
|
end
|
|
@@ -34,11 +34,11 @@ module Belt
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def welcome_css
|
|
37
|
-
@
|
|
37
|
+
@welcome_css ||= File.read(File.join(ASSETS_DIR, 'welcome.css'))
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def background_image_base64
|
|
41
|
-
@
|
|
41
|
+
@background_image_base64 ||= Base64.strict_encode64(
|
|
42
42
|
File.binread(File.join(ASSETS_DIR, 'belt-default.jpg'))
|
|
43
43
|
)
|
|
44
44
|
end
|
data/lib/belt/lambda_handler.rb
CHANGED
|
@@ -44,14 +44,14 @@ module Belt
|
|
|
44
44
|
|
|
45
45
|
# Auto-load all models (application_record first, then the rest)
|
|
46
46
|
models_dir = File.join(File.dirname(caller_file), 'models')
|
|
47
|
-
|
|
48
|
-
all_models = Dir[File.join(models_dir, '**', '*.rb')].sort
|
|
49
|
-
app_record = all_models.find { |f| File.basename(f) == 'application_record.rb' }
|
|
50
|
-
rest = all_models - [app_record].compact
|
|
47
|
+
return unless File.directory?(models_dir)
|
|
51
48
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
all_models = Dir[File.join(models_dir, '**', '*.rb')]
|
|
50
|
+
app_record = all_models.find { |f| File.basename(f) == 'application_record.rb' }
|
|
51
|
+
rest = all_models - [app_record].compact
|
|
52
|
+
|
|
53
|
+
require app_record if app_record
|
|
54
|
+
rest.each { |f| require f }
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
# API Gateway Lambda handler.
|
data/lib/belt/route_dsl.rb
CHANGED
|
@@ -51,12 +51,14 @@ module Belt
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
class NestedResourceBuilder
|
|
54
|
-
def initialize(gateway, prefix, collection_prefix, inherited_tables: [], inherited_auth: nil
|
|
54
|
+
def initialize(gateway, prefix, collection_prefix, inherited_tables: [], inherited_auth: nil, # rubocop:disable Metrics/ParameterLists
|
|
55
|
+
inherited_controller: nil)
|
|
55
56
|
@gateway = gateway
|
|
56
57
|
@prefix = prefix
|
|
57
58
|
@collection_prefix = collection_prefix
|
|
58
59
|
@inherited_tables = inherited_tables
|
|
59
60
|
@inherited_auth = inherited_auth
|
|
61
|
+
@inherited_controller = inherited_controller
|
|
60
62
|
end
|
|
61
63
|
|
|
62
64
|
def resources(name, options = {})
|
|
@@ -85,12 +87,13 @@ module Belt
|
|
|
85
87
|
end
|
|
86
88
|
|
|
87
89
|
def member(&)
|
|
88
|
-
MemberCollectionBuilder.new(@gateway, @prefix, @inherited_tables, @inherited_auth
|
|
90
|
+
MemberCollectionBuilder.new(@gateway, @prefix, @inherited_tables, @inherited_auth,
|
|
91
|
+
@inherited_controller).instance_eval(&)
|
|
89
92
|
end
|
|
90
93
|
|
|
91
94
|
def collection(&)
|
|
92
95
|
MemberCollectionBuilder.new(@gateway, @collection_prefix, @inherited_tables,
|
|
93
|
-
@inherited_auth).instance_eval(&)
|
|
96
|
+
@inherited_auth, @inherited_controller).instance_eval(&)
|
|
94
97
|
end
|
|
95
98
|
|
|
96
99
|
%i[get post put delete patch].each do |method|
|
|
@@ -111,16 +114,18 @@ module Belt
|
|
|
111
114
|
result[:tables] = (@inherited_tables + explicit_tables).uniq
|
|
112
115
|
end
|
|
113
116
|
result[:auth] ||= @inherited_auth if @inherited_auth
|
|
117
|
+
result[:controller] ||= @inherited_controller if @inherited_controller
|
|
114
118
|
result
|
|
115
119
|
end
|
|
116
120
|
end
|
|
117
121
|
|
|
118
122
|
class MemberCollectionBuilder
|
|
119
|
-
def initialize(gateway, prefix, inherited_tables, inherited_auth)
|
|
123
|
+
def initialize(gateway, prefix, inherited_tables, inherited_auth, inherited_controller = nil)
|
|
120
124
|
@gateway = gateway
|
|
121
125
|
@prefix = prefix
|
|
122
126
|
@inherited_tables = inherited_tables
|
|
123
127
|
@inherited_auth = inherited_auth
|
|
128
|
+
@inherited_controller = inherited_controller
|
|
124
129
|
end
|
|
125
130
|
|
|
126
131
|
%i[get post put delete patch].each do |method|
|
|
@@ -140,6 +145,7 @@ module Belt
|
|
|
140
145
|
result[:tables] = (@inherited_tables + explicit_tables).uniq
|
|
141
146
|
end
|
|
142
147
|
result[:auth] ||= @inherited_auth if @inherited_auth
|
|
148
|
+
result[:controller] ||= @inherited_controller if @inherited_controller
|
|
143
149
|
result
|
|
144
150
|
end
|
|
145
151
|
end
|
|
@@ -308,7 +314,11 @@ module Belt
|
|
|
308
314
|
previous_tables = @scope_tables
|
|
309
315
|
previous_controller = @scope_controller
|
|
310
316
|
|
|
311
|
-
|
|
317
|
+
# Nest path segments (Rails-style): scope path: "a" { scope path: "b" } → "a/b"
|
|
318
|
+
if options.key?(:path)
|
|
319
|
+
segment = options[:path].to_s.gsub(%r{^/|/$}, '')
|
|
320
|
+
@scope_prefix = @scope_prefix.to_s.empty? ? segment : "#{@scope_prefix}/#{segment}"
|
|
321
|
+
end
|
|
312
322
|
@scope_module = options[:module] || @scope_module
|
|
313
323
|
@scope_auth = options[:auth] || @scope_auth
|
|
314
324
|
@scope_tables = (@scope_tables + Array(options[:tables] || [])).uniq
|
|
@@ -338,14 +348,64 @@ module Belt
|
|
|
338
348
|
end
|
|
339
349
|
end
|
|
340
350
|
|
|
341
|
-
def resources(name, options = {}, &)
|
|
351
|
+
def resources(name, options = {}, &block)
|
|
342
352
|
options = apply_scope_options(options)
|
|
343
|
-
|
|
353
|
+
|
|
354
|
+
if @scope_prefix.empty?
|
|
355
|
+
@gateway.resources(name, options, &block)
|
|
356
|
+
else
|
|
357
|
+
# When inside a scope, generate routes with the prefix applied.
|
|
358
|
+
# Also set the controller explicitly so inference resolves correctly
|
|
359
|
+
# (e.g., scope "admin" + resources :users → controller "admin/users").
|
|
360
|
+
resource_name = name.to_s
|
|
361
|
+
singular = Belt::Inflector.singularize(resource_name)
|
|
362
|
+
param_name = options[:param] || "#{singular}_id"
|
|
363
|
+
controller = "#{@scope_prefix}/#{resource_name}"
|
|
364
|
+
resource_options = options.merge(route_type: :resources, controller: controller)
|
|
365
|
+
actions = determine_scoped_actions(options)
|
|
366
|
+
|
|
367
|
+
add_scoped_resource_routes(resource_name, param_name, resource_options, actions)
|
|
368
|
+
|
|
369
|
+
if block
|
|
370
|
+
collection_prefix = build_path("/#{resource_name}")
|
|
371
|
+
member_prefix = build_path("/#{resource_name}/{#{param_name}}")
|
|
372
|
+
resource_tables = Array(options[:tables] || [])
|
|
373
|
+
inherited_tables = (@gateway.default_tables + resource_tables).uniq
|
|
374
|
+
inherited_auth = options[:auth] || @gateway.default_auth
|
|
375
|
+
nested_builder = NestedResourceBuilder.new(@gateway, member_prefix, collection_prefix,
|
|
376
|
+
inherited_tables: inherited_tables,
|
|
377
|
+
inherited_auth: inherited_auth,
|
|
378
|
+
inherited_controller: controller)
|
|
379
|
+
nested_builder.instance_eval(&block)
|
|
380
|
+
end
|
|
381
|
+
end
|
|
344
382
|
end
|
|
345
383
|
|
|
346
384
|
def resource(name, options = {})
|
|
347
385
|
options = apply_scope_options(options)
|
|
348
|
-
|
|
386
|
+
|
|
387
|
+
if @scope_prefix.empty?
|
|
388
|
+
@gateway.resource(name, options)
|
|
389
|
+
else
|
|
390
|
+
resource_name = name.to_s
|
|
391
|
+
controller = "#{@scope_prefix}/#{resource_name}"
|
|
392
|
+
resource_options = options.merge(route_type: :resource, controller: controller)
|
|
393
|
+
actions = determine_scoped_actions(options, default: %i[show update destroy create])
|
|
394
|
+
|
|
395
|
+
@gateway.send(:add_route, :get, build_path("/#{resource_name}"), resource_options) if actions.include?(:show)
|
|
396
|
+
if actions.include?(:update)
|
|
397
|
+
@gateway.send(:add_route, :put, build_path("/#{resource_name}"),
|
|
398
|
+
resource_options)
|
|
399
|
+
end
|
|
400
|
+
if actions.include?(:destroy)
|
|
401
|
+
@gateway.send(:add_route, :delete, build_path("/#{resource_name}"),
|
|
402
|
+
resource_options)
|
|
403
|
+
end
|
|
404
|
+
if actions.include?(:create)
|
|
405
|
+
@gateway.send(:add_route, :post, build_path("/#{resource_name}"),
|
|
406
|
+
resource_options)
|
|
407
|
+
end
|
|
408
|
+
end
|
|
349
409
|
end
|
|
350
410
|
|
|
351
411
|
def lambda(name, &)
|
|
@@ -396,10 +456,35 @@ module Belt
|
|
|
396
456
|
@scope_prefix.empty? ? path : "/#{@scope_prefix}#{path}"
|
|
397
457
|
end
|
|
398
458
|
|
|
459
|
+
def determine_scoped_actions(options, default: %i[index create show update destroy])
|
|
460
|
+
if options[:only]
|
|
461
|
+
Array(options[:only])
|
|
462
|
+
elsif options[:except]
|
|
463
|
+
default - Array(options[:except])
|
|
464
|
+
else
|
|
465
|
+
default
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def add_scoped_resource_routes(resource_name, param_name, resource_options, actions)
|
|
470
|
+
@gateway.send(:add_route, :get, build_path("/#{resource_name}"), resource_options) if actions.include?(:index)
|
|
471
|
+
@gateway.send(:add_route, :post, build_path("/#{resource_name}"), resource_options) if actions.include?(:create)
|
|
472
|
+
if actions.include?(:show)
|
|
473
|
+
@gateway.send(:add_route, :get, build_path("/#{resource_name}/{#{param_name}}"), resource_options)
|
|
474
|
+
end
|
|
475
|
+
if actions.include?(:update)
|
|
476
|
+
@gateway.send(:add_route, :put, build_path("/#{resource_name}/{#{param_name}}"), resource_options)
|
|
477
|
+
end
|
|
478
|
+
return unless actions.include?(:destroy)
|
|
479
|
+
|
|
480
|
+
@gateway.send(:add_route, :delete, build_path("/#{resource_name}/{#{param_name}}"), resource_options)
|
|
481
|
+
end
|
|
482
|
+
|
|
399
483
|
def apply_scope_options(options)
|
|
400
484
|
result = options.dup
|
|
401
485
|
result[:auth] ||= @scope_auth if @scope_auth
|
|
402
486
|
result[:lambda] ||= @scope_module if @scope_module
|
|
487
|
+
result[:controller] ||= @scope_controller if @scope_controller
|
|
403
488
|
result[:tables] = (@scope_tables + Array(result[:tables] || [])).uniq if @scope_tables.any? || result[:tables]
|
|
404
489
|
result
|
|
405
490
|
end
|
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.12
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stowzilla
|
|
@@ -161,7 +161,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
161
161
|
requirements:
|
|
162
162
|
- - ">="
|
|
163
163
|
- !ruby/object:Gem::Version
|
|
164
|
-
version: '3.
|
|
164
|
+
version: '3.3'
|
|
165
165
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
166
166
|
requirements:
|
|
167
167
|
- - ">="
|